package com.google.sitebricks.client.transport;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Set;

import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.JsonToken;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.deser.CustomDeserializerFactory;
import org.codehaus.jackson.map.deser.StdDeserializerProvider;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.primitives.Primitives;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.sitebricks.conversion.Converter;
import com.google.sitebricks.conversion.ConverterRegistry;
import com.google.sitebricks.conversion.StandardTypeConverter;
import com.google.sitebricks.conversion.generics.GenericTypeReflector;

/**
 * @author Dhanji R. Prasanna (dhanji@gmail.com)
 * @author John Patterson (jdpatterson@gmail.com)
 * @author JRodriguez
 */
@Singleton
class JacksonJsonTransport extends Json {

  private final ObjectMapper objectMapper;
  private Collection<Class<?>> exceptions = Sets.newHashSet();
  
  @Inject
  public JacksonJsonTransport(ConverterRegistry registry) {
    this.objectMapper = new ObjectMapper();
    CustomDeserializerFactory deserializerFactory = new CustomDeserializerFactory();
    
    // leave these for Jackson to handle
    exceptions.add(String.class);
    exceptions.add(Object.class);
    exceptions.addAll(Primitives.allWrapperTypes());

    // 
    Multimap<Type, ConverterDirection> typeToConverterDirection = ArrayListMultimap.create();
    addConverterDirections(registry, true, typeToConverterDirection);
    addConverterDirections(registry, false, typeToConverterDirection);
    createJacksonDeserializers(deserializerFactory, typeToConverterDirection);
    
    objectMapper.setDeserializerProvider(new StdDeserializerProvider(deserializerFactory));
  }
  
  // keep track of which direction we want to use
  private static class ConverterDirection
  {
    Converter<?, ?> converter;
    boolean forward;
  }

  private void addConverterDirections(ConverterRegistry registry, boolean forward, Multimap<Type, ConverterDirection> typeToConverterDirections) {
    Multimap<Type, Converter<?, ?>> typeToConverters = forward ? registry.getConvertersByTarget() : registry.getConvertersBySource();
    Set<Type> types = typeToConverters.keySet();
    for (Type type : types) {
      if (exceptions.contains(type)) continue;
      Collection<Converter<?, ?>> converters = typeToConverters.get(type);
      for (Converter<?, ?> converter : converters) {
        ConverterDirection converterDirection = new ConverterDirection();
        converterDirection.converter = converter;
        converterDirection.forward = forward;
        typeToConverterDirections.put(type, converterDirection);
      }
    }
  }
  
  private void createJacksonDeserializers(CustomDeserializerFactory deserializerFactory, Multimap<Type, ConverterDirection> typeToConverterDirections)
  {
    Set<Type> targetTypes = typeToConverterDirections.keySet();
    for (Type targetType : targetTypes) {
      Collection<ConverterDirection> converterDirections = typeToConverterDirections.get(targetType);
      Class<?> targetClass = GenericTypeReflector.erase(targetType);
      ConvertersDeserializer jds = new ConvertersDeserializer(converterDirections);
      typesafeAddMapping(targetClass, jds, deserializerFactory);
    }
  }

  @SuppressWarnings("unchecked")
  private <T> void typesafeAddMapping(Class<?> type, JsonDeserializer<T> deserializer,
      CustomDeserializerFactory factory) {
    factory.addSpecificMapping((Class<T>) type, deserializer);
  }

  public <T> T in(InputStream in, Class<T> type) throws IOException {
    return objectMapper.readValue(in, type);
  }

  public <T> void out(OutputStream out, Class<T> type, T data) {
    try {
      objectMapper.writeValue(out, data);
    }
    catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  public class ConvertersDeserializer extends JsonDeserializer<Object> {

    private final Collection<ConverterDirection> converterDirections;

    public ConvertersDeserializer(Collection<ConverterDirection> converterDirections) {
      this.converterDirections = converterDirections;
    }

    public Object deserialize(JsonParser jp, DeserializationContext ctxt) 
        throws IOException, JsonProcessingException {

      Object source = getSourceObject(jp, ctxt);
      
      for (ConverterDirection converterDirection : converterDirections) {
        
        Type sourceType = converterDirection.forward ? 
            StandardTypeConverter.sourceType(converterDirection.converter) : 
            StandardTypeConverter.targetType(converterDirection.converter);
            
        // assume that Jackson only gives us non-generic types
        Class<?> converterSourceClass = GenericTypeReflector.erase(sourceType);
        
        if (converterSourceClass.isAssignableFrom(source.getClass())) {
          return converterDirection.forward ? 
              StandardTypeConverter.typeSafeTo(converterDirection.converter, source) : 
              StandardTypeConverter.typeSafeFrom(converterDirection.converter, source); 
        }
      }

      throw new IllegalStateException("Cannot convert from " + source);
    }

    private Object getSourceObject(JsonParser jp, DeserializationContext ctxt) throws JsonParseException, IOException {
        JsonToken t = jp.getCurrentToken();
        if (t == JsonToken.VALUE_NUMBER_INT) {
          return jp.getLongValue();
        }
        else if (t == JsonToken.VALUE_NUMBER_FLOAT) {
          return jp.getDoubleValue();
        }
        else if (t == JsonToken.VALUE_TRUE) {
          return Boolean.TRUE;
        }
        else if (t == JsonToken.VALUE_FALSE) {
            return Boolean.FALSE;
        }
        else if (t == JsonToken.VALUE_STRING) {
          return jp.getText();
        }
        else throw new IllegalStateException();
    }
  }
}

@@ -1307,16 +1307,23 @@
 ngleton%0A
+public 
 class Ja
@@ -2259,16 +2259,87 @@
 %0A  %7D%0A  %0A
+  public ObjectMapper getObjectMapper() %7B%0A%09return objectMapper;%0A  %7D%0A  %0A
   // kee

Make JacksonJsonTransport public so its ObjectMapper can be used elsewhere