package org.jboss.seam.remoting.wrapper;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

import org.dom4j.Element;
import org.hibernate.collection.PersistentCollection;

/**
 * Wrapper for collections, arrays, etc.
 *
 * @author Shane Bryzak
 */
public class BagWrapper extends BaseWrapper implements Wrapper
{
  private static final byte[] BAG_TAG_OPEN = "<bag>".getBytes();
  private static final byte[] BAG_TAG_CLOSE = "</bag>".getBytes();

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

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

    // Fix to prevent uninitialized lazy loading in Hibernate
    if (value instanceof PersistentCollection)
    {
      if (!((PersistentCollection) value).wasInitialized())
      {
        out.write(BAG_TAG_CLOSE);
        return;
      }
    }

    Collection vals = null;

    // If the value is an array, convert it to a Collection
    if (value.getClass().isArray())
    {
      vals = new ArrayList();
      for (int i = 0; i < Array.getLength(value); i++)
        vals.add(Array.get(value, i));
    }
    else if (Collection.class.isAssignableFrom(value.getClass()))
      vals = (Collection) value;
    else
      throw new RuntimeException(String.format(
        "Can not marshal object as bag: [%s]", value));

    for (Object val : vals)
    {
      out.write(ELEMENT_TAG_OPEN);
      context.createWrapperFromObject(val, path).marshal(out);
      out.write(ELEMENT_TAG_CLOSE);
    }

    out.write(BAG_TAG_CLOSE);
  }

  @SuppressWarnings("unchecked")
  public Object convert(Type type)
      throws ConversionException
  {
    // First convert the elements in the bag to a List of Wrappers
    List<Wrapper> vals = new ArrayList<Wrapper>();

    for (Element e : (List<Element>) element.elements("element"))
      vals.add(context.createWrapperFromElement((Element) e.elements().get(0)));

    if (type instanceof Class && ((Class) type).isArray())
    {
      Class arrayType = ((Class) type).getComponentType();
      value = Array.newInstance(arrayType, vals.size()); // Fix this
      for (int i = 0; i < vals.size(); i++)
        Array.set(value, i, vals.get(i).convert(arrayType));
    }
    else if (type instanceof Class && Collection.class.isAssignableFrom((Class) type))
    {
      try {
        value = getConcreteClass( (Class) type).newInstance();
      }
      catch (Exception ex) {
        throw new ConversionException(String.format(
            "Could not create instance of target type [%s].", type));
      }
      for (Wrapper w : vals)
        ((Collection) value).add(w.convert(Object.class));
    }
    else if (type instanceof ParameterizedType &&
             Collection.class.isAssignableFrom((Class) ((ParameterizedType) type).getRawType()))
    {
      Class rawType = (Class) ((ParameterizedType) type).getRawType();
      Type genType = Object.class;

      for (Type t : ((ParameterizedType) type).getActualTypeArguments())
      {
        genType = t;
        break;
      }

      try {
        value = getConcreteClass(rawType).newInstance();
      }
      catch (Exception ex) {
        throw new ConversionException(String.format(
            "Could not create instance of target type [%s].", rawType));
      }

      for (Wrapper w : vals)
        ((Collection) value).add(w.convert(genType));
    }

    return value;
  }

  private Class getConcreteClass(Class c)
  {
    if (c.isInterface())
    {
      // Support Set, Queue and (by default, and as a last resort) List
      if (Set.class.isAssignableFrom(c))
        return HashSet.class;
      else if (Queue.class.isAssignableFrom(c))
        return LinkedList.class;
      else
        return ArrayList.class;
    }
    else
      return c;
  }

  /**
   *
   * @param cls Class
   * @return ConversionScore
   */
  public ConversionScore conversionScore(Class cls)
  {
    // There's no such thing as an exact match for a bag, so we'll just look for
    // a compatible match

    if (cls.isArray())
      return ConversionScore.compatible;

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

    if (Collection.class.isAssignableFrom(cls))
      return ConversionScore.compatible;

    return ConversionScore.nomatch;
  }
}
