package org.jboss.seam.remoting.wrapper;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

/**
 * String wrapper class.
 *
 * @author Shane Bryzak
 */
public class StringWrapper extends BaseWrapper implements Wrapper
{
  private interface StringConverter {
    Object convert(String value);
  }

  private static final Map<Class,StringConverter> converters = new HashMap<Class,StringConverter>();

  static {
    converters.put(String.class, new StringConverter() {
      public Object convert(String value) { return value; }
    });
    converters.put(Object.class, new StringConverter() {
      public Object convert(String value) { return value; }
    });
    converters.put(StringBuilder.class, new StringConverter() {
      public Object convert(String value) { return new StringBuilder(value); }
    });
    converters.put(StringBuffer.class, new StringConverter() {
      public Object convert(String value) { return new StringBuffer(value); }
    });
    converters.put(Integer.class, new StringConverter() {
      public Object convert(String value) { return Integer.valueOf(value); }
    });
    converters.put(Integer.TYPE, new StringConverter() {
      public Object convert(String value) { return Integer.parseInt(value); }
    });
    converters.put(Long.class, new StringConverter() {
      public Object convert(String value) { return Long.valueOf(value); }
    });
    converters.put(Long.TYPE, new StringConverter() {
      public Object convert(String value) { return Long.parseLong(value); }
    });
    converters.put(Short.class, new StringConverter() {
      public Object convert(String value) { return Short.valueOf(value); }
    });
    converters.put(Short.TYPE, new StringConverter() {
      public Object convert(String value) { return Short.parseShort(value); }
    });
    converters.put(Boolean.class, new StringConverter() {
      public Object convert(String value) { return Boolean.valueOf(value); }
    });
    converters.put(Boolean.TYPE, new StringConverter() {
      public Object convert(String value) { return Boolean.parseBoolean(value); }
    });
    converters.put(Double.class, new StringConverter() {
      public Object convert(String value) { return Double.valueOf(value); }
    });
    converters.put(Double.TYPE, new StringConverter() {
      public Object convert(String value) { return Double.parseDouble(value); }
    });
    converters.put(Float.class, new StringConverter() {
      public Object convert(String value) { return Float.valueOf(value); }
    });
    converters.put(Float.TYPE, new StringConverter() {
      public Object convert(String value) { return Float.parseFloat(value); }
    });
    converters.put(Character.class, new StringConverter() {
      public Object convert(String value) { return Character.valueOf(value.charAt(0)); }
    });
    converters.put(Character.TYPE, new StringConverter() {
      public Object convert(String value) { return value.charAt(0); }
    });
    converters.put(Byte.class, new StringConverter() {
      public Object convert(String value) { return Byte.valueOf(value); }
    });
    converters.put(Byte.TYPE, new StringConverter() {
      public Object convert(String value) { return Byte.parseByte(value); }
    });
    converters.put(BigInteger.class, new StringConverter() {
      public Object convert(String value) { return new BigInteger(value); }
    });
    converters.put(BigDecimal.class, new StringConverter() {
      public Object convert(String value) { return new BigDecimal(value); }
    });
  }

  public static final String DEFAULT_ENCODING = "UTF-8";

  private static final byte[] STRING_TAG_OPEN = "<str>".getBytes();
  private static final byte[] STRING_TAG_CLOSE = "</str>".getBytes();

  private static final Class[] COMPATIBLE_CLASSES = {
      Integer.class, Integer.TYPE, Long.class, Long.TYPE,
      Short.class, Short.TYPE, Boolean.class, Boolean.TYPE,
      Double.class, Double.TYPE, Float.class, Float.TYPE,
      Character.class, Character.TYPE, Byte.class, Byte.TYPE,
      BigInteger.class, BigDecimal.class, Object.class};

  public Object convert(Type type)
      throws ConversionException
  {
    String elementValue = null;
    try {
      elementValue = URLDecoder.decode(element.getStringValue(),
                                              DEFAULT_ENCODING);
    }
    catch (UnsupportedEncodingException ex) {
      throw new ConversionException("Error converting value - encoding not supported.");
    }

    try
    {
      if (converters.containsKey(type))
        value = converters.get(type).convert(elementValue);
      else if (type instanceof Class && ( (Class) type).isEnum())
        value = Enum.valueOf( (Class) type, elementValue);
      else
        // Should never reach this line - calcConverstionScore should guarantee this.
        throw new ConversionException(String.format(
            "Value [%s] cannot be converted to type [%s].", elementValue, type));

      return value;
    }
    catch (Exception ex)
    {
      if (ex instanceof ConversionException)
        throw (ConversionException) ex;
      else
        throw new ConversionException(String.format("Could not convert value [%s] to type [%s].",
                                    elementValue, type.toString()), ex);
    }
  }

  public ConversionScore conversionScore(Class cls)
  {
    if (cls.equals(String.class) ||
        StringBuffer.class.isAssignableFrom(cls))
      return ConversionScore.exact;

    for (Class c : COMPATIBLE_CLASSES)
    {
      if (cls.equals(c))
        return ConversionScore.compatible;
    }

    if (cls.isEnum()) {
      try
      {
        String elementValue = URLDecoder.decode(element.getStringValue(),
                                                  DEFAULT_ENCODING);
        Enum.valueOf(cls, elementValue);
        return ConversionScore.compatible;
      }
      catch (IllegalArgumentException ex) { }
      catch (UnsupportedEncodingException ex) { }
    }

    return ConversionScore.nomatch;
  }

  public void marshal(OutputStream out)
    throws IOException
  {
    out.write(STRING_TAG_OPEN);
    out.write(URLEncoder.encode(value.toString(), DEFAULT_ENCODING).replace("+", "%20").getBytes());
    out.write(STRING_TAG_CLOSE);
  }
}
