/*
 * 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.core;

import java.util.*;
import java.lang.reflect.*;

import javax.management.*;

import jp.ossc.nimbus.beans.*;

/**
 * 汎用ファクトリサービスプロキシ。<p>
 *
 * @author M.Takata
 */
public class GenericsFactoryServiceProxy<T> extends FactoryServiceBase<T>
 implements DynamicMBean{
    
    private static final long serialVersionUID = -6799305487173484547L;
    
    private static final String MBEAN_SUFFIX = "MBean";
    private static final String MXBEAN_SUFFIX = "MXBean";
    
    protected Set<MBeanConstructorInfo> constructorInfos = new HashSet<MBeanConstructorInfo>();
    
    protected Map<String, MBeanAttributeInfo> attributeInfos = new HashMap<String, MBeanAttributeInfo>();
    
    protected Set<MBeanOperationInfo> operationInfos = new HashSet<MBeanOperationInfo>();
    
    protected MBeanInfo mbeanInfo;
    
    protected T template;
    
    protected ServiceMetaData metaData;
    
    protected Map<String, Property> attributePropCache = new HashMap<String, Property>();
    
    public GenericsFactoryServiceProxy(
        Class<?> clazz,
        boolean isManagement
    ) throws Exception{
        setManagement(isManagement);
        initConstructors();
        initBaseAttributes();
        initBaseOperations();
        if(isManagement){
            final Class<?> mbeanInterface = findMBeanInterface(clazz);
            if(mbeanInterface != null){
                initAttributesOf(mbeanInterface, clazz);
                initOperationsOf(mbeanInterface, clazz);
            }
        }
        initMBeanInfo();
    }
    
    protected void setServiceMetaData(ServiceMetaData metaData){
        this.metaData = metaData;
    }
    
    protected void initConstructors() throws Exception{
        for(Constructor<?> constructor : getClass().getConstructors()){
            if(constructor.getParameterTypes().length != 0){
                constructorInfos.add(
                    new MBeanConstructorInfo(null, constructor)
                );
            }
        }
    }
    
    protected void initBaseAttributes() throws Exception{
        initAttributesOf(FactoryServiceBaseMBean.class, getClass());
    }
    
    private boolean isDeclaredMethod(Method method, Class<?> clazz){
        try{
            final Method decMethod = clazz.getMethod(
                method.getName(),
                method.getParameterTypes()
            );
            
            // JRockitとSun JVMの動作の違いを吸収するための実装
            // JRockitでは、インタフェースのClassオブジェクトが
            // Objectクラスのサブクラスという扱いになっている
            if(Object.class.equals(decMethod.getDeclaringClass())){
                return false;
            }
        }catch(NoSuchMethodException e){
            return false;
        }
        return true;
    }
    
    protected void initAttributesOf(
        Class<?> mbeanInterface,
        Class<?> target
   ) throws Exception{
        for(SimpleProperty prop : SimpleProperty.getProperties(target)){
            if(!attributeInfos.containsKey(prop.getPropertyName())){
                
                if((prop.isReadable(target)
                        && isDeclaredMethod(prop.getReadMethod(target),
                                mbeanInterface))
                    || (prop.isWritable(target)
                        && isDeclaredMethod(prop.getWriteMethod(target),
                                mbeanInterface))
                ){
                    String propName = prop.getPropertyName();
                    if(Character.isLowerCase(propName.charAt(0))){
                        if(propName.length() == 1){
                            propName = Character.toString(
                                Character.toUpperCase(propName.charAt(0))
                            );
                        }else{
                            propName = Character.toUpperCase(propName.charAt(0))
                                + propName.substring(1);
                        }
                    }
                    attributeInfos.put(
                        propName,
                        new MBeanAttributeInfo(
                            propName,
                            null,
                            prop.isReadable(target)
                                 ? prop.getReadMethod(target) : null,
                            prop.isWritable(target)
                                 ? prop.getWriteMethod(target) : null
                        )
                    );
                }
            }
        }
    }
    
    protected void initBaseOperations() throws Exception{
        initOperationsOf(FactoryServiceBaseMBean.class, getClass());
    }
    
    protected void initOperationsOf(
        Class<?> mbeanInterface,
        Class<?> target
    ) throws Exception{
        for(Method method : mbeanInterface.getMethods()){
            boolean isOperationMethod = true;
            for(SimpleProperty prop : SimpleProperty.getProperties(target)){
                if(prop.isReadable(target)){
                    Method readMethod = prop.getReadMethod(target);
                    if(method.getName().equals(readMethod.getName())
                        && readMethod.getParameterTypes().length == 0){
                        isOperationMethod = false;
                        break;
                    }
                }
                if(prop.isWritable(target)){
                    Method writeMethod = prop.getWriteMethod(target);
                    if(method.getName().equals(writeMethod.getName())
                        && writeMethod.getParameterTypes().length == 1
                        && method.getParameterTypes()[0]
                            .equals(writeMethod.getParameterTypes()[0])
                    ){
                        isOperationMethod = false;
                        break;
                    }
                }
            }
            if(isOperationMethod){
                method = target.getMethod(
                    method.getName(),
                    method.getParameterTypes()
                );
                final MBeanOperationInfo oprationInfo
                     = new MBeanOperationInfo("", method);
                if(!operationInfos.contains(oprationInfo)){
                    operationInfos.add(oprationInfo);
                }
            }
        }
    }
    
    protected void initMBeanInfo(){
        mbeanInfo = new MBeanInfo(
            getClass().getName(),
            null,
            attributeInfos.values().toArray(
                new MBeanAttributeInfo[attributeInfos.size()]
            ),
            constructorInfos.toArray(
                new MBeanConstructorInfo[constructorInfos.size()]
            ),
            operationInfos.toArray(
                new MBeanOperationInfo[operationInfos.size()]
            ),
            new MBeanNotificationInfo[0]
        );
    }
    
    @Override
    public void startService() throws Exception{
        if(manager == null){
            throw new IllegalArgumentException("ServiceManager is null.");
        }
        if(metaData == null){
            final ServiceMetaData serviceData = manager.getServiceMetaData(name);
            if(serviceData == null){
                throw new IllegalArgumentException("ServiceMetaData is null.");
            }
            metaData = new ServiceMetaData(
                serviceData.getServiceLoader(),
                serviceData,
                serviceData.getManager()
            );
            metaData.setCode(serviceData.getCode());
            metaData.setConstructor(serviceData.getConstructor());
            for(FieldMetaData field : serviceData.getFields()){
                metaData.addField(field);
            }
            for(AttributeMetaData attr : serviceData.getAttributes()){
                metaData.addAttribute(attr);
            }
            for(InvokeMetaData invoke : serviceData.getInvokes()){
                metaData.addInvoke(invoke);
            }
        }
        
        if(managedInstances != null){
            for(T managedInstance : managedInstances){
                manager.startService((Service)managedInstance, metaData);
            }
        }
    }
    
    @Override
    public void stopService() throws Exception{
        if(managedInstances != null){
            for(T managedInstance : managedInstances){
                manager.stopService((Service)managedInstance, metaData);
            }
        }
        if(template != null){
            release(template);
            template = null;
        }
    }
    
    @Override
    public void destroyService() throws Exception{
        if(managedInstances != null){
            for(T managedInstance : managedInstances){
                manager.destroyService((Service)managedInstance, metaData);
            }
        }
        metaData = null;
    }
    
    // DynamicMBeanのJavaDoc
    @Override
    public Object getAttribute(String attribute)
     throws AttributeNotFoundException, MBeanException, ReflectionException{
        final MBeanAttributeInfo attributeInfo = attributeInfos.get(attribute);
        if(attributeInfo != null && attributeInfo.isReadable()){
            Property prop = attributePropCache.get(attribute);
            if(prop == null){
                prop = PropertyFactory.createProperty(attribute);
                attributePropCache.put(attribute, prop);
            }
            try{
                if(prop.isReadable(this)){
                    return prop.getProperty(this);
                }else{
                    if(template == null){
                        if(isCreateTemplateOnStart){
                            return null;
                        }else{
                            try{
                                createTemplate();
                            }catch(Exception e){
                                throw new ReflectionException(e);
                            }
                        }
                    }
                    
                    Object target = convertServiceToObject(template);
                    if(prop.isReadable(target)){
                        return prop.getProperty(target);
                    }else{
                        throw new AttributeNotFoundException(attribute);
                    }
                }
            }catch(NoSuchPropertyException e){
                throw new AttributeNotFoundException(attribute);
            }catch(InvocationTargetException e){
                throw new ReflectionException(e);
            }
        }
        throw new AttributeNotFoundException(attribute);
    }
    
    // DynamicMBeanのJavaDoc
    @Override
    public AttributeList getAttributes(String[] attributes){
        final AttributeList list = new AttributeList();
        for(String attr : attributes){
            try{
                list.add(
                    new Attribute(
                        attr,
                        getAttribute(attr)
                    )
                );
            }catch(AttributeNotFoundException e){
            }catch(MBeanException e){
            }catch(ReflectionException e){
            }
        }
        return list;
    }
    
    // DynamicMBeanのJavaDoc
    @Override
    public void setAttribute(Attribute attribute)
     throws AttributeNotFoundException, InvalidAttributeValueException,
            MBeanException, ReflectionException{
        final MBeanAttributeInfo attributeInfo
             = attributeInfos.get(attribute.getName());
        if(attributeInfo != null && attributeInfo.isWritable()){
            if(metaData != null){
                AttributeMetaData attrData
                     = metaData.getAttribute(attribute.getName());
                if(attrData == null
                     && attribute.getName().length() > 1
                ){
                    StringBuilder tmpName = new StringBuilder();
                    if(Character.isLowerCase(attribute.getName().charAt(0))){
                        tmpName.append(
                            Character.toUpperCase(attribute.getName().charAt(0))
                        );
                    }else{
                        tmpName.append(
                            Character.toLowerCase(attribute.getName().charAt(0))
                        );
                    }
                    if(attribute.getName().length() > 2){
                        tmpName.append(attribute.getName().substring(1));
                    }
                    attrData = metaData.getAttribute(tmpName.toString());
                }
                if(attrData != null){
                    attrData.setValue(attribute.getValue());
                }
            }
            final SimpleProperty prop = (SimpleProperty)PropertyFactory
                .createProperty(attribute.getName());
            Exception targetException = null;
            try{
                if(prop.isWritable(getClass())){
                    try{
                        prop.setProperty(this, attribute.getValue());
                    }catch(InvocationTargetException e){
                        targetException = e;
                    }
                }else{
                    if(template == null){
                        if(!isManagement()){
                            return;
                        }else{
                            try{
                                createTemplate();
                            }catch(Exception e){
                                throw new ReflectionException(e);
                            }
                        }
                    }
                    final Object target = convertServiceToObject(template);
                    try{
                        prop.setProperty(target, attribute.getValue());
                    }catch(InvocationTargetException e){
                        targetException = e;
                    }
                    if(managedInstances == null
                         || managedInstances.size() == 0){
                        return;
                    }
                    for(T managedInstance : managedInstances){
                        final Object instance = convertServiceToObject(
                            (Service)managedInstance
                        );
                        if(prop.isWritable(instance.getClass())){
                            try{
                                prop.setProperty(
                                    instance,
                                    attribute.getValue()
                                );
                            }catch(InvocationTargetException e){
                                targetException = e;
                            }
                        }else{
                            throw new AttributeNotFoundException(
                                "class=" + instance.getClass().getName()
                                    + ", attributename=" + attribute.getName()
                            );
                        }
                    }
                }
            }catch(NoSuchPropertyException e){
                throw new AttributeNotFoundException(
                    "name=" + attribute.getName()
                        + ", value=" + attribute.getValue()
                );
            }finally{
                if(targetException != null){
                    throw new ReflectionException(targetException);
                }
            }
        }
    }
    
    // DynamicMBeanのJavaDoc
    @Override
    public AttributeList setAttributes(AttributeList attributes){
        final AttributeList list = new AttributeList();
        for(int i = 0, max = attributes.size(); i < max; i++){
            
            try{
                setAttribute((Attribute)attributes.get(i));
                list.add(attributes.get(i));
            }catch(AttributeNotFoundException e){
            }catch(InvalidAttributeValueException e){
            }catch(MBeanException e){
            }catch(ReflectionException e){
            }
        }
        return list;
    }
    
    // DynamicMBeanのJavaDoc
    @Override
    public Object invoke(
        String actionName,
        Object[] params,
        String[] signature
    ) throws MBeanException, ReflectionException{
        
        Class<?>[] paramTypes = null;
        if(signature != null){
            paramTypes = new Class[signature.length];
            try{
                for(int i = 0; i < signature.length; i++){
                    paramTypes[i] = Utility.convertStringToClass(signature[i]);
                }
            }catch(ClassNotFoundException e){
                throw new ReflectionException(e);
            }
        }
        Method method = null;
        
        try{
            method = getClass().getMethod(actionName, paramTypes);
            return method.invoke(this, params);
        }catch(NoSuchMethodException e){
            if(!isManagement()){
                return null;
            }
        }catch(IllegalAccessException e){
            throw new ReflectionException(e);
        }catch(InvocationTargetException e){
            throw new ReflectionException(e);
        }
        if(template == null){
            if(isCreateTemplateOnStart){
                return null;
            }else{
                try{
                    createTemplate();
                }catch(Exception e){
                    throw new ReflectionException(e);
                }
            }
        }
        final Object target = convertServiceToObject(template);
        
        Object ret = null;
        Exception targetException = null;
        try{
            method = target.getClass().getMethod(actionName, paramTypes);
            ret = method.invoke(target, params);
        }catch(NoSuchMethodException e){
            throw new ReflectionException(e);
        }catch(IllegalAccessException e){
            throw new ReflectionException(e);
        }catch(InvocationTargetException e){
            targetException = e;
        }
        if(managedInstances == null
             || managedInstances.size() == 0){
            return ret;
        }
        for(T managedInstance : managedInstances){
            Object instance = convertServiceToObject((Service)managedInstance);
            try{
                method = instance.getClass().getMethod(actionName, paramTypes);
                ret = method.invoke(instance, params);
            }catch(NoSuchMethodException e){
                throw new ReflectionException(e);
            }catch(IllegalAccessException e){
                throw new ReflectionException(e);
            }catch(InvocationTargetException e){
                targetException = e;
            }
        }
        if(targetException != null){
            throw new ReflectionException(targetException);
        }
        return ret;
    }
    
    // DynamicMBeanのJavaDoc
    @Override
    public MBeanInfo getMBeanInfo(){
        return mbeanInfo;
    }
    
    @Override
    public void release(Object obj){
        if(obj instanceof Service
             && (obj != template
                 || !isManagement()
                 || getState() == State.STOPPING)
        ){
            final Service service = (Service)obj;
            if(manager == null){
                service.stop();
                service.destroy();
            }else{
                manager.stopService(service, metaData);
                manager.destroyService(service, metaData);
            }
        }
        super.release(obj);
    }
    
    /**
     * このファクトリが提供するオブジェクトのインスタンスを生成する。<p>
     * {@link ServiceManager#instanciateService(ServiceMetaData)}で生成したサービスのインスタンスに対して、以下の処理を行う。<br>
     * <ol>
     *   <li>{@link #isManagement()}がtrueの場合、生成したサービスにサービス名とサービスマネージャ名を設定する。この際、サービス名は、このファクトリサービスのサービス名の後ろに"$" + "管理されている生成したサービスの通し番号"を付与したものである。また、サービスマネージャ名は、このファクトリサービスのサービスマネージャ名と同じである。</li>
     *   <li>生成したサービスの生成処理（{@link Service#create()}）。</li>
     *   <li>生成したサービスの開始処理（{@link Service#start()}）。</li>
     *   <li>生成したサービスが{@link ServiceBase}を継承している場合は、このファクトリサービスに設定されている{@link jp.ossc.nimbus.service.log.Logger Logger}と{@link jp.ossc.nimbus.service.message.MessageRecordFactory MessageRecordFactory}を、生成したサービスにも設定する。</li>
     * </ol>
     *
     * @return このファクトリが提供するオブジェクトのインスタンス
     * @exception Exception 生成中に例外が発生した場合
     */
    @Override
    protected final T createInstance() throws Exception{
        return createInstance(false);
    }
    
    @Override
    protected T createTemplate() throws Exception{
        if(isManagement() && template != null){
            return template;
        }
        return createInstance(true);
    }
    
    @SuppressWarnings("unchecked")
    protected final T createInstance(boolean isTemplate) throws Exception{
        if(manager == null || metaData == null){
            return null;
        }
        T obj = null;
        if(isManagement()){
            obj = (T)manager.instanciateService(metaData);
        }else{
            obj = (T)manager.createObject(metaData);
        }
        if(obj == null){
            return null;
        }
        if(obj instanceof Service){
            final Service service = (Service)obj;
            if(!isTemplate && isManagement() && getServiceManagerName() != null){
                service.setServiceManagerName(getServiceManagerName());
                service.setServiceName(
                    getServiceName() + '$' + getManagedInstanceSet().size()
                );
            }else{
                service.setServiceManagerName(null);
                service.setServiceName(null);
            }
            if(service.getState() == State.DESTROYED){
                manager.createService(service, metaData);
            }
            if(service instanceof ServiceBase){
                final ServiceBase base = (ServiceBase)service;
                if(manager != null){
                    base.logger.setDefaultLogger(manager.getLogger());
                    if(getSystemLoggerServiceName() == null){
                        base.logger.setLogger(manager.getLogger());
                    }
                    base.message.setDefaultMessageRecordFactory(
                        manager.getMessageRecordFactory()
                    );
                    if(getSystemMessageRecordFactoryServiceName() == null){
                        base.message.setMessageRecordFactory(
                            manager.getMessageRecordFactory()
                        );
                    }
                }
                if(getSystemLoggerServiceName() != null){
                    base.setSystemLoggerServiceName(
                        getSystemLoggerServiceName()
                    );
                }
                if(getSystemMessageRecordFactoryServiceName() != null){
                    base.setSystemMessageRecordFactoryServiceName(
                        getSystemMessageRecordFactoryServiceName()
                    );
                }
            }
            if(service.getState() == State.CREATED){
                manager.startService(service, metaData);
            }
        }
        if(isTemplate && isManagement()){
            template = obj;
        }
        return obj;
    }
    
    private Object convertServiceToObject(Object service){
        Object target = service;
        while(target instanceof ServiceProxy){
            Object child = ((ServiceProxy)target).getTarget();
            if(child == target){
                break;
            }
            target = child;
        }
        return target;
    }
    
    /**
     * Service内のログ出力に使用する{@link jp.ossc.nimbus.service.log.Logger}サービスの名前を設定する。<p>
     * {@link #getManagedInstanceSet()}を呼び出して、このファクトリが管理しているサービスを取得し、サービスが{@link ServiceBase}のインスタンスであれば、{@link ServiceBase#setSystemLoggerServiceName(ServiceName)}を呼び出す。<br>
     *
     * @param name Service内のログ出力に使用する{@link jp.ossc.nimbus.service.log.Logger}サービスの名前
     * @see #getSystemLoggerServiceName()
     */
    @Override
    public void setSystemLoggerServiceName(ServiceName name){
        super.setSystemLoggerServiceName(name);
        if(managedInstances != null){
            for(T service : managedInstances){
                if(service instanceof ServiceBase){
                    ((ServiceBase)service).setSystemLoggerServiceName(name);
                }
            }
        }
    }
    
    /**
     * Service内でのメッセージ取得に使用する{@link jp.ossc.nimbus.service.message.MessageRecordFactory}サービスの名前を設定する。<p>
     * {@link #getManagedInstanceSet()}を呼び出して、このファクトリが管理しているサービスを取得し、サービスが{@link ServiceBase}のインスタンスであれば、{@link ServiceBase#setSystemMessageRecordFactoryServiceName(ServiceName)}を呼び出す。<br>
     *
     * @param name Service内でのメッセージ取得に使用する{@link jp.ossc.nimbus.service.message.MessageRecordFactory}サービスの名前
     * @see #getSystemMessageRecordFactoryServiceName()
     */
    @Override
    public void setSystemMessageRecordFactoryServiceName(
        final ServiceName name
    ){
        super.setSystemMessageRecordFactoryServiceName(name);
        if(managedInstances != null){
            for(T service : managedInstances){
                if(service instanceof ServiceBase){
                    ((ServiceBase)service)
                        .setSystemMessageRecordFactoryServiceName(name);
                }
            }
        }
    }
    
    private static Class<?> findMBeanInterface(Class<?> clazz){
        if(clazz == null){
            return null;
        }
        final String className = clazz.getName();
        final String mbeanInterfaceName = className + MBEAN_SUFFIX;
        final Class<?>[] interfaces = clazz.getInterfaces();
        for(Class<?> itf : interfaces){
            if(itf.equals(DynamicMBean.class)
                || itf.getName().equals(mbeanInterfaceName)
                || itf.getName().endsWith(MXBEAN_SUFFIX)
            ){
                return itf;
            }
        }
        return findMBeanInterface(clazz.getSuperclass());
    }
}