/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.myfaces.extensions.scripting.jsf.annotation;

import org.apache.myfaces.config.RuntimeConfig;
import org.apache.myfaces.config.element.NavigationRule;
import org.apache.myfaces.config.impl.digester.elements.ManagedBean;
import org.apache.myfaces.extensions.scripting.core.api.AnnotationScanListener;
import org.apache.myfaces.extensions.scripting.core.common.util.ReflectUtil;
import org.apache.myfaces.extensions.scripting.core.common.util.StringUtils;

import javax.faces.bean.*;
import java.lang.reflect.Field;
import java.util.*;
import java.util.logging.Level;

/**
 * bean implementation listener which registers new java sources
 * into the runtime config, note this class is not thread safe
 * it is only allowed to be called from a single thread
 *
 * @author Werner Punz (latest modification by $Author$)
 * @version $Revision$ $Date$
 */

public class MyFacesBeanImplementationListener extends BaseAnnotationScanListener implements AnnotationScanListener
{

    public boolean supportsAnnotation(String annotation) {
        return annotation.equals(javax.faces.bean.ManagedBean.class.getName());
    }

    public boolean supportsAnnotation(Class annotation) {
        return annotation.equals(javax.faces.bean.ManagedBean.class);
    }

    public void register(Class clazz, java.lang.annotation.Annotation ann) {


        javax.faces.bean.ManagedBean annCasted = (javax.faces.bean.ManagedBean) ann;

        String beanName = annCasted.name();
        if (StringUtils.isBlank(beanName)) {
            beanName = normalizeName(clazz.getName());
        }

        beanName = beanName.replaceAll("\"", "");
        //we need to reregister for every bean due to possible managed prop
        //and scope changes
        registerManagedBean(clazz, beanName);

    }

    /**
     * registers a new managed bean into runtime config
     * @param clazz
     * @param beanName
     */
    protected void registerManagedBean(Class clazz, String beanName)
    {
        RuntimeConfig config = getRuntimeConfig();

        ManagedBean mbean;
        if (!hasToReregister(beanName, clazz)) {
            mbean = (ManagedBean) _alreadyRegistered.get(beanName);
            //return;
        } else {
            mbean = new ManagedBean();
        }

        mbean.setBeanClass(clazz.getName());

        ReflectUtil.setField(mbean, "beanClass", null, true);
        mbean.setName(beanName);
        handleManagedpropertiesCompiled(mbean, fields(clazz));
        resolveScope(clazz, mbean);

        _alreadyRegistered.put(beanName, mbean);
        config.addManagedBean(beanName, mbean);
    }

    private void resolveScope(Class clazz, ManagedBean mbean) {
        //now lets resolve the scope
        String scope = "none";
        if (clazz.isAnnotationPresent(RequestScoped.class)) {
            scope = "request";
        } else if (clazz.isAnnotationPresent(SessionScoped.class)) {
            scope = "session";
        } else if (clazz.isAnnotationPresent(ApplicationScoped.class)) {
            scope = "application";
        } else if (clazz.isAnnotationPresent(ViewScoped.class)) {
            scope = "view";    
        } else if (clazz.isAnnotationPresent(NoneScoped.class)) {
            scope = "none";
        } else if (clazz.isAnnotationPresent(CustomScoped.class)) {
            CustomScoped customScoped = (CustomScoped) clazz.getAnnotation(CustomScoped.class);
            scope = (customScoped != null) ? customScoped.value() : "custom";
        }
        mbean.setScope(scope);
    }

    protected void handleManagedpropertiesCompiled(ManagedBean mbean, Field[] fields) {
        /*since we reprocess the managed properties we can handle them here by clearing them first*/
        mbean.getManagedProperties().clear();
        for (Field field : fields) {
            if (_log.isLoggable(Level.FINEST)) {
                _log.log(Level.FINEST, "  Scanning field '" + field.getName() + "'");
            }
            ManagedProperty property = field
                    .getAnnotation(ManagedProperty.class);
            if (property != null) {
                if (_log.isLoggable(Level.FINE)) {
                    _log.log(Level.FINE, "  Field '" + field.getName()
                            + "' has a @ManagedProperty annotation");
                }

                org.apache.myfaces.config.impl.digester.elements.ManagedProperty mpc =
                        new org.apache.myfaces.config.impl.digester.elements.ManagedProperty();
                String name = property.name();
                if ((name == null) || "".equals(name)) {
                    name = field.getName();
                }
                mpc.setPropertyName(name);
                mpc.setPropertyClass(field.getType().getName()); // FIXME - primitives, arrays, etc.
                mpc.setValue(property.value());

                ReflectUtil.executeMethod(mbean, "addProperty", mpc);
            }
        }
    }

    /**
     * <p>Return an array of all <code>Field</code>s reflecting declared
     * fields in this class, or in any superclass other than
     * <code>java.lang.Object</code>.</p>
     *
     * @param clazz Class to be analyzed
     * @return the array of fields
     */
    private Field[] fields(Class clazz) {

        Map<String, Field> fields = new HashMap<String, Field>();
        do {
            for (Field field : clazz.getDeclaredFields()) {
                if (!fields.containsKey(field.getName())) {
                    fields.put(field.getName(), field);
                }
            }
        } while ((clazz = clazz.getSuperclass()) != Object.class);
        return fields.values().toArray(new Field[fields.size()]);
    }

    protected boolean hasToReregister(String name, Class clazz) {
        ManagedBean mbean = (ManagedBean) _alreadyRegistered.get(name);
        return mbean == null || !mbean.getManagedBeanClassName().equals(clazz.getName());
    }

    /**
     * name normalizer for automated name mapping
     * (aka if no name attribute is given in the annotation)
     *
     * @param className the classname to be mapped (can be with package=
     * @return the normalized jsf bean name
     */
    private String normalizeName(String className) {
        String name = className.substring(className.lastIndexOf(".") + 1);

        return name.substring(0, 1).toLowerCase() + name.substring(1);
    }

    @SuppressWarnings("unchecked")
    public void purge(String className) {
        RuntimeConfig config = getRuntimeConfig();
        //We have to purge and readd our managed beans, unfortunatly the myfaces impl enforces
        //us to do the same for the nav rules after purge
        //we cannot purge the managed beans and nav rules separately
        Collection<NavigationRule> navigationRules = new ArrayList<NavigationRule>();
        Map managedBeans = new HashMap<String, org.apache.myfaces.config.element.ManagedBean>();

        navigationRules.addAll(config.getNavigationRules());
        managedBeans.putAll(config.getManagedBeans());

        config.purge();

        for (NavigationRule navRule : navigationRules) {
            config.addNavigationRule(navRule);
        }

        //We refresh the managed beans, dead references still can cause
        //runtime errors but in this case we cannot do anything
        org.apache.myfaces.config.element.ManagedBean mbeanFound = null;
        List<String> mbeanKey = new LinkedList<String>();

        for (Object entry : managedBeans.entrySet()) {
            Map.Entry mbean = (Map.Entry) entry;

            Object bean =  mbean.getValue();


            if (!((Class)ReflectUtil.executeMethod( bean, "getManagedBeanClass")).getName().equals(className)) {
                config.addManagedBean((String) mbean.getKey(), (org.apache.myfaces.config.element.ManagedBean) mbean.getValue());
            } else {
                Object mbeanf = mbean.getValue();
                mbeanKey.add((String)ReflectUtil.executeMethod(mbeanf, "getManagedBeanName"));
            }
        }
        if (mbeanFound != null) {
            for (String toRemove : mbeanKey) {
                _alreadyRegistered.remove(toRemove);
            }
        }
    }
}
