package org.jboss.seam.remoting.wrapper;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.dom4j.Element;

/**
 * @author Shane Bryzak
 */
public class MapWrapper extends BaseWrapper implements Wrapper
{
   private static final byte[] MAP_TAG_OPEN = "<map>".getBytes();
   private static final byte[] MAP_TAG_CLOSE = "</map>".getBytes();

   private static final byte[] ELEMENT_TAG_OPEN = "<element>".getBytes();
   private static final byte[] ELEMENT_TAG_CLOSE = "</element>".getBytes();

   private static final byte[] KEY_TAG_OPEN = "<k>".getBytes();
   private static final byte[] KEY_TAG_CLOSE = "</k>".getBytes();

   private static final byte[] VALUE_TAG_OPEN = "<v>".getBytes();
   private static final byte[] VALUE_TAG_CLOSE = "</v>".getBytes();

   public void marshal(OutputStream out) throws IOException
   {
      out.write(MAP_TAG_OPEN);

      Map m = (Map) this.value;

      for (Object key : m.keySet())
      {
         out.write(ELEMENT_TAG_OPEN);

         out.write(KEY_TAG_OPEN);
         context.createWrapperFromObject(key, String.format("%s[key]", path)).marshal(out);
         out.write(KEY_TAG_CLOSE);

         out.write(VALUE_TAG_OPEN);
         context.createWrapperFromObject(m.get(key), String.format("%s[value]", path)).marshal(out);
         out.write(VALUE_TAG_CLOSE);

         out.write(ELEMENT_TAG_CLOSE);
      }

      out.write(MAP_TAG_CLOSE);
   }

   public Object convert(Type type) throws ConversionException
   {
      if (context == null)
      {
         throw new IllegalStateException("No call context has been set");
      }
      
      Class typeClass = null;
      Type keyType = null;
      Type valueType = null;

      // Either the type should be a generified Map
      if (type instanceof ParameterizedType && Map.class.isAssignableFrom((Class) ((ParameterizedType) type).getRawType()))
      {
         typeClass = (Class) ((ParameterizedType) type).getRawType();

         for (Type t : ((ParameterizedType) type).getActualTypeArguments())
         {
            if (keyType == null)
               keyType = t;
            else
            {
               valueType = t;
               break;
            }
         }
      }
      // Or a non-generified Map
      else if (type instanceof Class && Map.class.isAssignableFrom((Class) type))
      {
         if (!((Class) type).isInterface())
            typeClass = (Class) type;
         keyType = Object.class;
         valueType = Object.class;
      }
      // If it's neither, throw an exception
      else
         throw new ConversionException(String.format("Cannot convert value to type [%s]", type));

      // If we don't have a concrete type, default to creating a HashMap
      if (typeClass == null || typeClass.isInterface())
         value = new HashMap();
      else
      {
         try
         {
            // Otherwise create an instance of the concrete type
            if (type instanceof Class)
            {
               value = ((Class) type).newInstance();
            }
            else if (type instanceof ParameterizedType)
            {
               value = ((Class) ((ParameterizedType) type).getRawType()).newInstance();
            }
         }
         catch (Exception ex)
         {
            throw new ConversionException(String.format("Could not create value of type [%s]", type));
         }
      }

      for (Element e : (List<Element>) element.elements("element"))
      {
         Element keyElement = (Element) e.element("k").elementIterator().next();
         Element valueElement = (Element) e.element("v").elementIterator().next();

         ((Map) value).put(context.createWrapperFromElement(keyElement).convert(keyType), 
               context.createWrapperFromElement(valueElement).convert(valueType));
      }

      return value;
   }

   public ConversionScore conversionScore(Class cls)
   {
      if (Map.class.isAssignableFrom(cls))
         return ConversionScore.exact;

      if (cls.equals(Object.class))
         return ConversionScore.compatible;

      return ConversionScore.nomatch;
   }
}
