package org.jboss.seam.remoting.wrapper;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;

import org.dom4j.Element;
import org.jboss.seam.Component;
import org.jboss.seam.Seam;
import org.jboss.seam.remoting.InterfaceGenerator;
import org.jboss.seam.util.Reflections;

/**
 * @author Shane Bryzak
 */
public class BeanWrapper extends BaseWrapper implements Wrapper
{
  private static final byte[] REF_START_TAG_OPEN = "<ref id=\"".getBytes();
  private static final byte[] REF_START_TAG_END = "\"/>".getBytes();

  private static final byte[] BEAN_START_TAG_OPEN = "<bean type=\"".getBytes();
  private static final byte[] BEAN_START_TAG_CLOSE = "\">".getBytes();
  private static final byte[] BEAN_CLOSE_TAG = "</bean>".getBytes();

  private static final byte[] MEMBER_START_TAG_OPEN = "<member name=\"".getBytes();
  private static final byte[] MEMBER_START_TAG_CLOSE = "\">".getBytes();
  private static final byte[] MEMBER_CLOSE_TAG = "</member>".getBytes();

  @Override
  public void setElement(Element element)
  {
    super.setElement(element);

    String beanType = element.attributeValue("type");

    Component component = Component.forName(beanType);

    if (component != null)
    {
      value = component.newInstance();
    }
    else
    {
      try {
        value = Reflections.classForName(beanType).newInstance();
      }
      catch (Exception ex) {
        throw new RuntimeException("Could not unmarshal bean element: " + element.getText(), ex);
      }
    }
  }

  @Override
  public void unmarshal()
  {
    List members = element.elements("member");

    for (Element member : (List<Element>) members)
    {
      String name = member.attributeValue("name");

      Wrapper w = context.createWrapperFromElement((Element) member.elementIterator().next());

      Class cls = value.getClass();

      // We're going to try a combination of ways to set the property value here
      Method method = null;
      Field field = null;
              
      // First try to find the best matching method
      String setter = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
      
      ConversionScore score = ConversionScore.nomatch;
      for (Method m : cls.getMethods())
      {
         if (setter.equals(m.getName()) && m.getParameterTypes().length == 1)
         {
            ConversionScore s = w.conversionScore(m.getParameterTypes()[0]);
            if (s.getScore() > score.getScore())
            {
               method = m;
               score = s;
            }
         }
      }    
      
      // If we can't find a method, look for a matching field name
      if (method == null)
      {
         while (field == null && !cls.equals(Object.class))
         {
           try
           {
             // First check the declared fields
             field = cls.getDeclaredField(name);
           }
           catch (NoSuchFieldException ex)
           {
             // Couldn't find the field.. try the superclass
             cls = cls.getSuperclass();
           }
         }

         if (field == null)
         {
           throw new RuntimeException(String.format(
             "Error while unmarshalling - property [%s] not found in class [%s]",
             name, value.getClass().getName()));
         }      
      }
      
      // Now convert the field value to the correct target type
      Object fieldValue = null;
      try {
        fieldValue = w.convert(method != null ? method.getGenericParameterTypes()[0] : 
           field.getGenericType());
      }
      catch (ConversionException ex) {
        throw new RuntimeException("Could not convert value while unmarshaling", ex);
      }

      // If we have a setter method, invoke it
      if (method != null)
      {
         try
         {
            method.invoke(value, fieldValue);
         }
         catch (Exception e)
         {
            throw new RuntimeException(String.format(
                     "Could not invoke setter method [%s]", method.getName()));
         }
      }
      else
      {
         // Otherwise try to set the field value directly
         boolean accessible = field.isAccessible();
         try
         {
           if (!accessible)
             field.setAccessible(true);
           field.set(value, fieldValue);
         }
         catch (Exception ex)
         {
           throw new RuntimeException("Could not set field value.", ex);
         }
         finally
         {
           field.setAccessible(accessible);
         }
      }
    }
  }

  public Object convert(Type type)
      throws ConversionException
  {
    if (type instanceof Class && ((Class) type).isAssignableFrom(value.getClass()))
      return value;
    else
      throw new ConversionException(String.format(
        "Value [%s] cannot be converted to type [%s].", value, type));
  }

  public void marshal(OutputStream out)
    throws IOException
  {
    context.addOutRef(this);

    out.write(REF_START_TAG_OPEN);
    out.write(Integer.toString(context.getOutRefs().indexOf(this)).getBytes());
    out.write(REF_START_TAG_END);
  }

  @Override
  public void serialize(OutputStream out)
      throws IOException
  {
    serialize(out, null);
  }

  public void serialize(OutputStream out, List<String> constraints)
    throws IOException
  {
    out.write(BEAN_START_TAG_OPEN);

    Class cls = value.getClass();

    /** @todo This is a hack to get the "real" class - find out if there is
              an API method in CGLIB that can be used instead */
    if (cls.getName().contains("EnhancerByCGLIB"))
    {
       cls = cls.getSuperclass();
    }
    
    if (cls.getName().contains("_$$_javassist_"))
    {
       cls=cls.getSuperclass();
    }
    
    String componentName = Seam.getComponentName(cls);
    Component component = componentName != null ? Component.forName(componentName) : null;
    
    if (component != null) 
       cls = component.getBeanClass();       

    if (componentName != null)
      out.write(componentName.getBytes());
    else
      out.write(cls.getName().getBytes());

    out.write(BEAN_START_TAG_CLOSE);

    for (String propertyName : InterfaceGenerator.getAccessibleProperties(cls))
    {
      String fieldPath = path != null && path.length() > 0 ? String.format("%s.%s", path, propertyName) : propertyName;

      // Also exclude fields listed using wildcard notation: [componentName].fieldName
      String wildCard = String.format("[%s].%s", componentName != null ? componentName : cls.getName(), propertyName);

      if (constraints == null || (!constraints.contains(fieldPath) && !constraints.contains(wildCard)))
      {
        out.write(MEMBER_START_TAG_OPEN);
        out.write(propertyName.getBytes());
        out.write(MEMBER_START_TAG_CLOSE);

        Field f = null;
        try
        {
          f = cls.getField(propertyName);
        }
        catch (NoSuchFieldException ex) { }

        boolean accessible = false;
        try
        {
          // Temporarily set the field's accessibility so we can read it
          if (f != null)
          {
            accessible = f.isAccessible();
            f.setAccessible(true);
            context.createWrapperFromObject(f.get(value),
                fieldPath).marshal(out);
          }
          else
          {
            Method accessor = null;
            try
            {
              accessor = cls.getMethod(String.format("get%s%s",
                  Character.toUpperCase(propertyName.charAt(0)),
                  propertyName.substring(1)));
            }
            catch (NoSuchMethodException ex)
            {
              try
              {
                accessor = cls.getMethod(String.format("is%s%s",
                    Character.toUpperCase(propertyName.charAt(0)),
                    propertyName.substring(1)));
              }
              catch (NoSuchMethodException ex2)
              {
                // uh oh... continue with the next one
                continue;
              }
            }

            try
            {
              context.createWrapperFromObject(accessor.invoke(value), fieldPath).
                  marshal(out);
            }
            catch (InvocationTargetException ex)
            {
              throw new RuntimeException(String.format(
                  "Failed to read property [%s] for object [%s]",
                  propertyName, value));
            }
          }
        }
        catch (IllegalAccessException ex)
        {
          throw new RuntimeException("Error reading value from field.");
        }
        finally
        {
          if (f != null)
            f.setAccessible(accessible);
        }

        out.write(MEMBER_CLOSE_TAG);
      }
    }

    out.write(BEAN_CLOSE_TAG);
  }

  public ConversionScore conversionScore(Class cls) {
    if (cls.equals(value.getClass()))
      return ConversionScore.exact;
    else if (cls.isAssignableFrom(value.getClass()) || cls.equals(Object.class))
      return ConversionScore.compatible;
    else
      return ConversionScore.nomatch;
  }
}
