/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.xb.builder.runtime;

import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import javax.xml.namespace.QName;

import org.jboss.beans.info.spi.BeanAccessMode;
import org.jboss.beans.info.spi.BeanInfo;
import org.jboss.beans.info.spi.PropertyInfo;
import org.jboss.config.spi.Configuration;
import org.jboss.reflect.spi.ClassInfo;
import org.jboss.reflect.spi.ConstructorInfo;
import org.jboss.reflect.spi.TypeInfo;
import org.jboss.xb.annotations.JBossXmlMapEntry;
import org.jboss.xb.annotations.JBossXmlMapKey;
import org.jboss.xb.annotations.JBossXmlMapValue;
import org.jboss.xb.spi.BeanAdapter;

/**
 * MapPropertyHandler.
 * 
 * @author <a href="alex@jboss.com">Alexey Loubyansky</a>
 * @author <a href="ales.justin@jboss.com">Ales Justin</a>
 * @version $Revision: 1.1 $
 */
public class MapPropertyHandler extends AbstractPropertyHandler
{
   private final MapFactory mapFactory;
   private final MapPutAdapter mapPutAdapter;

   private TypeInfo keyType;
   private TypeInfo valueType;

   /**
    * Create a new MapPropertyHandler.
    *
    * @param config the configuration
    * @param propertyInfo the property
    * @param propertyType the property type
    * @param wrapped the wrapped flag
    * @throws IllegalArgumentException for a null qName or property
    */
   public MapPropertyHandler(Configuration config, BeanAccessMode beanAccessMode, PropertyInfo propertyInfo, TypeInfo propertyType, boolean wrapped)
   {
      super(propertyInfo, propertyType);

      ClassInfo classInfo = (ClassInfo) propertyType;
      keyType = classInfo.getKeyType();
      valueType = classInfo.getValueType();

      if(wrapped)
      {
         mapFactory = null;
      }
      else
      {
         if (Modifier.isAbstract(classInfo.getModifiers()))
         {
            mapFactory = HashMapFactory.INSTANCE;
         }
         else
         {
            ConstructorInfo constructor = classInfo.getDeclaredConstructor(null);
            if (constructor == null)
            {
               for (ConstructorInfo ctor : classInfo.getDeclaredConstructors())
               {
                  if (ctor.getParameterTypes().length == 0)
                  {
                     log.warn("ClassInfo.getDeclaredConstructor(null) didn't work for " + classInfo.getName()
                           + ", found the default ctor in ClassInfo.getDeclaredConstructors()");
                     constructor = ctor;
                     break;
                  }
               }

               if (constructor == null)
               {
                  throw new RuntimeException("Default constructor not found for " + classInfo.getName());
               }
            }
            mapFactory = new CtorMapFactory(constructor);
         }
      }
      
      JBossXmlMapEntry entry = propertyInfo.getUnderlyingAnnotation(JBossXmlMapEntry.class);
      if(entry == null)
         entry =((ClassInfo)propertyType).getUnderlyingAnnotation(JBossXmlMapEntry.class);
      
      if(entry != null && !JBossXmlMapEntry.DEFAULT.class.equals(entry.type()))
      {
         BeanInfo entryBean = config.getBeanInfo(entry.type(), beanAccessMode);
         mapPutAdapter = new CustomMapEntryPutAdapter(entryBean);
      }
      else
         mapPutAdapter = new DefaultMapEntryPutAdapter();
   }

   @Override
   @SuppressWarnings("unchecked")
   public void handle(PropertyInfo propertyInfo, TypeInfo propertyType, Object parent, Object child, QName qName)
   {
      if(trace)
         log.trace("handle entry " + qName + ", property=" + propertyInfo.getName() + ", parent=" + parent + ", child=" + child);
      
      BeanAdapter beanAdapter = (BeanAdapter) parent;
      
      Map<Object, Object> m = null;
      if(mapFactory == null)
      {
         // it's wrapped, so the parent expected to be a map
         m = (Map<Object, Object>) beanAdapter.getValue();
      }
      else
      {
         try
         {
            if (propertyInfo.getGetter() != null)
               m = (Map<Object, Object>) beanAdapter.get(propertyInfo);
         }
         catch (Throwable t)
         {
            throw new RuntimeException("QName " + qName + " error getting map property " + propertyInfo.getName()
                  + " for " + BuilderUtil.toDebugString(parent), t);
         }

         // No map so create one
         if (m == null)
         {
            try
            {
               m = mapFactory.createMap();
            }
            catch (Throwable t)
            {
               throw new RuntimeException("QName " + qName + " error creating map: " + propertyType.getName(), t);
            }

            try
            {
               beanAdapter.set(propertyInfo, m);
            }
            catch (Throwable t)
            {
               throw new RuntimeException("QName " + qName + " error setting map property " + propertyInfo.getName()
                     + " for " + BuilderUtil.toDebugString(parent) + " with value " + BuilderUtil.toDebugString(m), t);
            }
         }
      }
      
      try
      {
         mapPutAdapter.put(m, child);
      }
      catch (Throwable e)
      {
         throw new RuntimeException("QName " + qName + " error adding " + BuilderUtil.toDebugString(child) + " to map " + BuilderUtil.toDebugString(m), e);
      }
   }

   private interface MapPutAdapter
   {
      void put(Map<Object,Object> map, Object entry) throws Throwable;
   }
   
   private class CustomMapEntryPutAdapter implements MapPutAdapter
   {
      private final PropertyInfo keyProp;
      private final PropertyInfo valueProp;
      
      CustomMapEntryPutAdapter(BeanInfo entryBean)
      {
         PropertyInfo keyProp = null;
         PropertyInfo valueProp = null;
         for(PropertyInfo prop : entryBean.getProperties())
         {
            JBossXmlMapKey key = prop.getUnderlyingAnnotation(JBossXmlMapKey.class);
            if(key != null)
            {
               if(keyProp != null)
                  throw new IllegalStateException(
                        "Found two properties in entry type " + entryBean.getName() +
                        " annotated with @JBossXmlMapKey: " +
                        keyProp.getName() + " and " + prop.getName());
               keyProp = prop;
            }

            JBossXmlMapValue value = prop.getUnderlyingAnnotation(JBossXmlMapValue.class);
            if(value != null)
            {
               if(valueProp != null)
                  throw new IllegalStateException(
                        "Found two properties in entry type " + entryBean.getName() +
                        " annotated with @JBossXmlMapValue: " +
                        valueProp.getName() + " and " + prop.getName());
               valueProp = prop;
            }
         }

         if(keyProp == null)
            throw new IllegalStateException(
                  "Entry type " + entryBean.getName() +
                  " doesn't have any property annotated with @JBossXmlMapKey.");
         
         this.keyProp = keyProp;
         this.valueProp = valueProp;
      }
      
      public void put(Map<Object, Object> map, Object entry) throws Throwable
      {
         Object key = keyProp.get(entry);
         Object value = entry;
         if(valueProp != null)
            value = valueProp.get(entry);

         checkPut(map, key, value);
      }
   }
   
   private class DefaultMapEntryPutAdapter implements MapPutAdapter
   {
      public void put(Map<Object, Object> map, Object entry)
      {
         if(!(entry instanceof DefaultMapEntry))
            throw new IllegalStateException("Expected DefaultMapEntry but got " + entry);

         DefaultMapEntry defEntry = (DefaultMapEntry) entry;
         Object key = defEntry.getKey();
         Object value = defEntry.getValue();

         checkPut(map, key, value);
      }
   }

   /**
    * Check values before they are put into map.
    *
    * @param map the map
    * @param key the key
    * @param value the value
    */
   protected void checkPut(Map<Object, Object> map, Object key, Object value)
   {
      if (keyType != null && key!= null && keyType.isInstance(key) == false)
         throw new IllegalArgumentException("Key is not an instance of " + keyType + ", key: " + key);
      if (valueType != null && value != null && valueType.isInstance(value) == false)
         throw new IllegalArgumentException("Value is not an instance of " + valueType + ", value: " + value);
      map.put(key, value);
   }

   private static interface MapFactory
   {
      Map<Object, Object> createMap() throws Throwable;
   }
   
   private static class HashMapFactory implements MapFactory
   {
      static final MapFactory INSTANCE = new HashMapFactory();
      
      @SuppressWarnings("unchecked")
      public Map<Object, Object> createMap()
      {
         return new HashMap<Object, Object>();
      }  
   }
   
   private static class CtorMapFactory implements MapFactory
   {
      private final ConstructorInfo ctor;
      
      CtorMapFactory(ConstructorInfo ctor)
      {
         this.ctor = ctor;
      }
      
      @SuppressWarnings("unchecked")
      public Map createMap() throws Throwable
      {
         return (Map) ctor.newInstance(null);
      }      
   }
}
