/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * 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.seasar.framework.container.impl;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javassist.runtime.Desc;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyFactory.ClassLoaderProvider;
import ognl.OgnlRuntime;

import org.seasar.framework.container.ComponentDef;
import org.seasar.framework.container.ComponentNotFoundRuntimeException;
import org.seasar.framework.container.ContainerConstants;
import org.seasar.framework.container.ContainerNotRegisteredRuntimeException;
import org.seasar.framework.container.CyclicReferenceRuntimeException;
import org.seasar.framework.container.ExternalContext;
import org.seasar.framework.container.ExternalContextComponentDefRegister;
import org.seasar.framework.container.MetaDef;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.TooManyRegistrationComponentDef;
import org.seasar.framework.container.ognl.S2ContainerPropertyAccessor;
import org.seasar.framework.container.util.MetaDefSupport;
import org.seasar.framework.container.util.S2ContainerUtil;
import org.seasar.framework.container.util.Traversal;
import org.seasar.framework.log.Logger;
import org.seasar.framework.util.CaseInsensitiveMap;
import org.seasar.framework.util.StringUtil;

/**
 * {@link S2Container}の実装クラスです。
 * 
 * @author higa
 * 
 */
public class S2ContainerImpl implements S2Container, ContainerConstants {

    private static final Logger logger = Logger
            .getLogger(S2ContainerImpl.class);

    private Map componentDefMap = new HashMap();

    private List componentDefList = new ArrayList();

    private String namespace;

    private String path;

    private boolean initializeOnCreate;

    private List children = new ArrayList();

    private Map childPositions = new HashMap();

    private List parents = new ArrayList();

    private CaseInsensitiveMap descendants = new CaseInsensitiveMap();

    private S2Container root;

    private ExternalContext externalContext;

    private ExternalContextComponentDefRegister externalContextComponentDefRegister;

    private MetaDefSupport metaDefSupport = new MetaDefSupport(this);

    private boolean inited = false;

    private ClassLoader classLoader = null;

    static {
        OgnlRuntime.setPropertyAccessor(S2Container.class,
                new S2ContainerPropertyAccessor());
        Desc.useContextClassLoader = true;
        ProxyFactory.classLoaderProvider = new ClassLoaderProvider() {
            public ClassLoader get(ProxyFactory proxyFactory) {
                return Thread.currentThread().getContextClassLoader();
            }
        };
    }

    /**
     * {@link S2ContainerImpl}を作成します。
     */
    public S2ContainerImpl() {
        root = this;
        register0(new SimpleComponentDef(this, CONTAINER_NAME));
        classLoader = Thread.currentThread().getContextClassLoader();
    }

    public S2Container getRoot() {
        return root;
    }

    public void setRoot(S2Container root) {
        this.root = root != null ? root : this;
    }

    /**
     * @see org.seasar.framework.container.S2Container#getComponent(java.lang.Object)
     */
    public Object getComponent(Object componentKey) {
        assertParameterIsNotNull(componentKey, "componentKey");
        ComponentDef cd = S2ContainerBehavior.acquireFromGetComponent(this,
                componentKey);
        if (cd == null) {
            return null;
        }
        return cd.getComponent();
    }

    /**
     * @see org.seasar.framework.container.S2Container#getComponent(java.lang.Object)
     */
    public Object[] findComponents(Object componentKey) {
        assertParameterIsNotNull(componentKey, "componentKey");
        ComponentDef[] componentDefs = findComponentDefs(componentKey);
        return toComponentArray(componentKey, componentDefs);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.seasar.framework.container.S2Container#findAllComponents(java.lang.Object)
     */
    public Object[] findAllComponents(Object componentKey)
            throws CyclicReferenceRuntimeException {
        assertParameterIsNotNull(componentKey, "componentKey");
        ComponentDef[] componentDefs = findAllComponentDefs(componentKey);
        return toComponentArray(componentKey, componentDefs);
    }

    /**
     * @see org.seasar.framework.container.S2Container#findLocalComponents(java.lang.Object)
     */
    public Object[] findLocalComponents(Object componentKey)
            throws CyclicReferenceRuntimeException {
        assertParameterIsNotNull(componentKey, "componentKey");
        ComponentDef[] componentDefs = findLocalComponentDefs(componentKey);
        return toComponentArray(componentKey, componentDefs);
    }

    /**
     * コンポーネントの配列に変換します。
     * 
     * @param componentKey
     * @param componentDefs
     * @return コンポーネントの配列
     */
    protected Object[] toComponentArray(Object componentKey,
            ComponentDef[] componentDefs) {
        int length = componentDefs.length;
        Object[] components = (componentKey instanceof Class) ? (Object[]) Array
                .newInstance((Class) componentKey, length)
                : new Object[length];
        for (int i = 0; i < length; ++i) {
            components[i] = componentDefs[i].getComponent();
        }
        return components;
    }

    /**
     * @see org.seasar.framework.container.S2Container#injectDependency(java.lang.Object)
     */
    public void injectDependency(Object outerComponent) {
        injectDependency(outerComponent, outerComponent.getClass());
    }

    /**
     * @see org.seasar.framework.container.S2Container#injectDependency(java.lang.Object,
     *      java.lang.Class)
     */
    public void injectDependency(Object outerComponent, Class componentClass) {
        assertParameterIsNotNull(outerComponent, "outerComponent");
        assertParameterIsNotNull(componentClass, "componentClass");
        ComponentDef cd = S2ContainerBehavior.acquireFromInjectDependency(this,
                componentClass);
        if (cd != null) {
            cd.injectDependency(outerComponent);
        }
    }

    /**
     * @see org.seasar.framework.container.S2Container#injectDependency(java.lang.Object,
     *      java.lang.String)
     */
    public void injectDependency(Object outerComponent, String componentName) {
        assertParameterIsNotNull(outerComponent, "outerComponent");
        assertParameterIsNotEmpty(componentName, "componentName");
        ComponentDef cd = S2ContainerBehavior.acquireFromInjectDependency(this,
                componentName);
        if (cd != null) {
            cd.injectDependency(outerComponent);
        }
    }

    /**
     * @see org.seasar.framework.container.S2Container#register(java.lang.Object)
     */
    public void register(Object component) {
        assertParameterIsNotNull(component, "component");
        register(new SimpleComponentDef(component));
    }

    public void register(Object component, String componentName) {
        assertParameterIsNotNull(component, "component");
        assertParameterIsNotEmpty(componentName, "componentName");
        register(new SimpleComponentDef(component, componentName));
    }

    /**
     * @see org.seasar.framework.container.S2Container#register(java.lang.Class)
     */
    public void register(Class componentClass) {
        assertParameterIsNotNull(componentClass, "componentClass");
        register(new ComponentDefImpl(componentClass));
    }

    /**
     * @see org.seasar.framework.container.S2Container#register(java.lang.Class,
     *      java.lang.String)
     */
    public void register(Class componentClass, String componentName) {
        assertParameterIsNotNull(componentClass, "componentClass");
        assertParameterIsNotEmpty(componentName, "componentName");
        register(new ComponentDefImpl(componentClass, componentName));
    }

    /**
     * @see org.seasar.framework.container.S2Container#register(org.seasar.framework.container.ComponentDef)
     */
    public void register(ComponentDef componentDef) {
        assertParameterIsNotNull(componentDef, "componentDef");
        register0(componentDef);
        componentDefList.add(componentDef);
    }

    /**
     * {@link ComponentDef}を登録します。
     * 
     * @param componentDef
     */
    public void register0(ComponentDef componentDef) {
        if (componentDef.getContainer() == null) {
            componentDef.setContainer(this);
        }
        registerByClass(componentDef);
        registerByName(componentDef);
    }

    /**
     * {@link Class}をキーにして {@link ComponentDef}を登録します。
     * 
     * @param componentDef
     */
    protected void registerByClass(ComponentDef componentDef) {
        Class[] classes = S2ContainerUtil.getAssignableClasses(componentDef
                .getComponentClass());
        for (int i = 0; i < classes.length; ++i) {
            registerMap(classes[i], componentDef);
        }
    }

    /**
     * 名前をキーにして {@link ComponentDef}を登録します。
     * 
     * @param componentDef
     */
    protected void registerByName(ComponentDef componentDef) {
        String componentName = componentDef.getComponentName();
        if (componentName != null) {
            registerMap(componentName, componentDef);
        }
    }

    /**
     * キャッシュに {@link ComponentDef}を登録します。
     * 
     * @param key
     * @param componentDef
     */
    protected void registerMap(Object key, ComponentDef componentDef) {
        registerMap(key, componentDef, this);
    }

    public void registerMap(Object key, ComponentDef componentDef,
            S2Container container) {
        int position = getContainerPosition(container);
        ComponentDefHolder holder = (ComponentDefHolder) componentDefMap
                .get(key);
        if (holder == null) {
            holder = new ComponentDefHolder(position, componentDef);
            componentDefMap.put(key, holder);
        } else if (position > holder.getPosition()) {
            return;
        } else if (position < holder.getPosition()) {
            holder.setPosition(position);
            holder.setComponentDef(componentDef);
        } else if (container != this) {
            holder.setComponentDef(componentDef);
        } else {
            holder.setComponentDef(createTooManyRegistration(key, holder
                    .getComponentDef(), componentDef));
        }

        registerParent(key, holder.getComponentDef());
    }

    /**
     * {@link S2Container}のネストが深くなってもパフォーマンスが落ちないように親に {@link ComponentDef}を登録します。
     * 
     * @param key
     * @param componentDef
     */
    protected void registerParent(Object key, ComponentDef componentDef) {
        for (int i = 0; i < getParentSize(); i++) {
            S2Container parent = (S2Container) getParent(i);
            parent.registerMap(key, componentDef, this);
            if (isNeedNS(key, componentDef)) {
                parent
                        .registerMap(namespace + NS_SEP + key, componentDef,
                                this);
            }
        }
    }

    /**
     * @see org.seasar.framework.container.S2Container#getComponentDefSize()
     */
    public int getComponentDefSize() {
        return componentDefList.size();
    }

    /**
     * @see org.seasar.framework.container.S2Container#getComponentDef(int)
     */
    public ComponentDef getComponentDef(int index) {
        return (ComponentDef) componentDefList.get(index);
    }

    /**
     * @see org.seasar.framework.container.S2Container#getComponentDef(java.lang.Object)
     */
    public ComponentDef getComponentDef(Object key)
            throws ComponentNotFoundRuntimeException {
        assertParameterIsNotNull(key, "key");
        return S2ContainerBehavior.acquireFromGetComponentDef(this, key);
    }

    /**
     * @see org.seasar.framework.container.S2Container#findComponentDefs(java.lang.Object)
     */
    public ComponentDef[] findComponentDefs(Object key)
            throws ComponentNotFoundRuntimeException {
        assertParameterIsNotNull(key, "key");
        ComponentDef cd = internalGetComponentDef(key);
        return toComponentDefArray(cd);
    }

    /**
     * @see org.seasar.framework.container.S2Container#findAllComponentDefs(java.lang.Object)
     */
    public ComponentDef[] findAllComponentDefs(final Object componentKey) {
        assertParameterIsNotNull(componentKey, "componentKey");
        final List componentDefs = new ArrayList();
        Traversal.forEachContainer(this, new Traversal.S2ContainerHandler() {
            public Object processContainer(S2Container container) {
                componentDefs.addAll(Arrays.asList(container
                        .findLocalComponentDefs(componentKey)));
                return null;
            }
        });
        return (ComponentDef[]) componentDefs
                .toArray(new ComponentDef[componentDefs.size()]);
    }

    /**
     * @see org.seasar.framework.container.S2Container#findLocalComponentDefs(java.lang.Object)
     */
    public ComponentDef[] findLocalComponentDefs(Object componentKey) {
        ComponentDefHolder holder = (ComponentDefHolder) componentDefMap
                .get(componentKey);
        if (holder == null || holder.getPosition() > 0) {
            return new ComponentDef[0];
        }
        return toComponentDefArray(holder.getComponentDef());
    }

    /**
     * {@link ComponentDef}の配列に変換します。
     * 
     * @param cd
     * @return {@link ComponentDef}の配列
     */
    protected ComponentDef[] toComponentDefArray(ComponentDef cd) {
        if (cd == null) {
            return new ComponentDef[0];
        } else if (cd instanceof TooManyRegistrationComponentDefImpl) {
            return ((TooManyRegistrationComponentDefImpl) cd)
                    .getComponentDefs();
        }
        return new ComponentDef[] { cd };
    }

    /**
     * 内部的なgetComponentDefの実装です。
     * 
     * @param key
     * @return {@link ComponentDef}
     */
    protected ComponentDef internalGetComponentDef(Object key) {
        ComponentDefHolder holder = (ComponentDefHolder) componentDefMap
                .get(key);
        if (holder != null) {
            return holder.getComponentDef();
        }
        if (key instanceof String) {
            String name = (String) key;
            int index = name.indexOf(NS_SEP);
            if (index > 0) {
                String ns = name.substring(0, index);
                name = name.substring(index + 1);
                if (ns.equals(namespace)) {
                    return internalGetComponentDef(name);
                }
            }
        }
        return null;
    }

    /**
     * @see org.seasar.framework.container.S2Container#hasComponentDef(java.lang.Object)
     */
    public boolean hasComponentDef(Object componentKey) {
        assertParameterIsNotNull(componentKey, "componentKey");
        return S2ContainerBehavior.acquireFromHasComponentDef(this,
                componentKey) != null;
    }

    /**
     * @see org.seasar.framework.container.S2Container#hasDescendant(java.lang.String)
     */
    public boolean hasDescendant(String path) {
        assertParameterIsNotEmpty(path, "path");
        return descendants.containsKey(path);
    }

    public S2Container getDescendant(String path) {
        S2Container descendant = (S2Container) descendants.get(path);
        if (descendant != null) {
            return descendant;
        }
        throw new ContainerNotRegisteredRuntimeException(path);
    }

    public void registerDescendant(S2Container descendant) {
        assertParameterIsNotNull(descendant, "descendant");
        descendants.put(descendant.getPath(), descendant);
    }

    /**
     * @see org.seasar.framework.container.S2Container#include(org.seasar.framework.container.S2Container)
     */
    public void include(S2Container child) {
        assertParameterIsNotNull(child, "child");
        children.add(child);
        childPositions.put(child, new Integer(children.size()));
        child.setRoot(getRoot());
        child.addParent(this);
    }

    /**
     * 子供の位置を返します。
     * 
     * @param container
     * @return 子供の位置
     */
    protected int getContainerPosition(S2Container container) {
        if (container == this) {
            return 0;
        }
        return ((Integer) childPositions.get(container)).intValue();
    }

    /**
     * 名前空間が必要かどうかを返します。
     * 
     * @param key
     * @param cd
     * @return 名前空間が必要かどうか
     */
    protected boolean isNeedNS(Object key, ComponentDef cd) {
        return key instanceof String && namespace != null;
    }

    public int getChildSize() {
        return children.size();
    }

    public S2Container getChild(int index) {
        return (S2Container) children.get(index);
    }

    public int getParentSize() {
        return parents.size();
    }

    public S2Container getParent(int index) {
        return (S2Container) parents.get(index);
    }

    public void addParent(S2Container parent) {
        parents.add(parent);
        for (Iterator it = componentDefMap.entrySet().iterator(); it.hasNext();) {
            Entry entry = (Entry) it.next();
            Object key = entry.getKey();
            ComponentDefHolder holder = (ComponentDefHolder) entry.getValue();
            ComponentDef cd = holder.getComponentDef();
            parent.registerMap(key, cd, this);
            if (isNeedNS(key, cd)) {
                parent.registerMap(namespace + NS_SEP + key, cd, this);
            }
        }
    }

    public void init() {
        if (inited) {
            return;
        }
        final ExternalContextComponentDefRegister register = getRoot()
                .getExternalContextComponentDefRegister();
        if (register != null) {
            register.registerComponentDefs(this);
        }
        final ClassLoader currentLoader = Thread.currentThread()
                .getContextClassLoader();
        Thread.currentThread().setContextClassLoader(classLoader);
        try {
            for (int i = 0; i < getChildSize(); ++i) {
                getChild(i).init();
            }
            for (int i = 0; i < getComponentDefSize(); ++i) {
                getComponentDef(i).init();
            }
            inited = true;
        } finally {
            Thread.currentThread().setContextClassLoader(currentLoader);
        }
    }

    public void destroy() {
        if (!inited) {
            return;
        }
        final ClassLoader currentLoader = Thread.currentThread()
                .getContextClassLoader();
        Thread.currentThread().setContextClassLoader(classLoader);
        try {
            for (int i = getComponentDefSize() - 1; 0 <= i; --i) {
                try {
                    getComponentDef(i).destroy();
                } catch (Throwable t) {
                    logger.error("ESSR0017", t);
                }
            }
            for (int i = getChildSize() - 1; 0 <= i; --i) {
                getChild(i).destroy();
            }

            componentDefMap = null;
            componentDefList = null;
            namespace = null;
            path = null;
            children = null;
            childPositions = null;
            parents = null;
            descendants = null;
            externalContext = null;
            externalContextComponentDefRegister = null;
            metaDefSupport = null;
            classLoader = null;
            root = this;
            inited = false;
        } finally {
            Thread.currentThread().setContextClassLoader(currentLoader);
        }
    }

    public String getNamespace() {
        return namespace;
    }

    public void setNamespace(String namespace) {
        componentDefMap.remove(namespace);
        this.namespace = namespace;
        registerMap(namespace, new S2ContainerComponentDef(this, namespace));
    }

    public boolean isInitializeOnCreate() {
        return initializeOnCreate;
    }

    public void setInitializeOnCreate(boolean initializeOnCreate) {
        this.initializeOnCreate = initializeOnCreate;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public ExternalContext getExternalContext() {
        return externalContext;
    }

    public void setExternalContext(ExternalContext externalContext) {
        this.externalContext = externalContext;
    }

    public ExternalContextComponentDefRegister getExternalContextComponentDefRegister() {
        return externalContextComponentDefRegister;
    }

    public void setExternalContextComponentDefRegister(
            ExternalContextComponentDefRegister register) {
        this.externalContextComponentDefRegister = register;
    }

    public void addMetaDef(MetaDef metaDef) {
        metaDefSupport.addMetaDef(metaDef);
    }

    public MetaDef getMetaDef(int index) {
        return metaDefSupport.getMetaDef(index);
    }

    public MetaDef getMetaDef(String name) {
        return metaDefSupport.getMetaDef(name);
    }

    public MetaDef[] getMetaDefs(String name) {
        return metaDefSupport.getMetaDefs(name);
    }

    public int getMetaDefSize() {
        return metaDefSupport.getMetaDefSize();
    }

    public ClassLoader getClassLoader() {
        return classLoader;
    }

    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /**
     * {@link TooManyRegistrationComponentDef}を作成します。
     * 
     * @param key
     * @param currentComponentDef
     * @param newComponentDef
     * @return {@link TooManyRegistrationComponentDef}
     */
    public static ComponentDef createTooManyRegistration(Object key,
            ComponentDef currentComponentDef, ComponentDef newComponentDef) {

        if (currentComponentDef instanceof TooManyRegistrationComponentDef) {
            ((TooManyRegistrationComponentDef) currentComponentDef)
                    .addComponentDef(newComponentDef);
            return currentComponentDef;
        } else {
            TooManyRegistrationComponentDef tmrcf = new TooManyRegistrationComponentDefImpl(
                    key);
            tmrcf.addComponentDef(currentComponentDef);
            tmrcf.addComponentDef(newComponentDef);
            return tmrcf;
        }
    }

    /**
     * パラメータが<code>null</code>でないことを表明します。
     * 
     * @param parameter
     * @param name
     */
    protected void assertParameterIsNotNull(Object parameter, String name) {
        if (parameter == null) {
            throw new IllegalArgumentException(name);
        }
    }

    /**
     * パラメータが空でないことを表明します。
     * 
     * @param parameter
     * @param name
     */
    protected void assertParameterIsNotEmpty(String parameter, String name) {
        if (StringUtil.isEmpty(parameter)) {
            throw new IllegalArgumentException(name);
        }
    }

    /**
     * 子供の位置を保持するクラスです。
     */
    static class ComponentDefHolder {
        private int position;

        private ComponentDef componentDef;

        /**
         * {@link ComponentDefHolder}を作成します。
         * 
         * @param position
         * @param componentDef
         */
        public ComponentDefHolder(int position, ComponentDef componentDef) {
            this.position = position;
            this.componentDef = componentDef;
        }

        /**
         * 位置を返します。
         * 
         * @return 位置
         */
        public int getPosition() {
            return position;
        }

        /**
         * 位置を設定します。
         * 
         * @param position
         */
        public void setPosition(int position) {
            this.position = position;
        }

        /**
         * {@link ComponentDef}を返します。
         * 
         * @return {@link ComponentDef}
         */
        public ComponentDef getComponentDef() {
            return componentDef;
        }

        /**
         * {@link ComponentDef}を設定します。
         * 
         * @param componentDef
         */
        public void setComponentDef(ComponentDef componentDef) {
            this.componentDef = componentDef;
        }
    }

}