package com.google.sitebricks.binding;

import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.sitebricks.Evaluator;
import com.google.sitebricks.rendering.Strings;
import net.jcip.annotations.Immutable;
import org.mvel2.PropertyAccessException;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author Dhanji R. Prasanna (dhanji@gmail.com)
 */
@Immutable
@Singleton
class MvelRequestBinder implements RequestBinder {
  private final Evaluator evaluator;
  private final Provider<FlashCache> cacheProvider;
  private final Logger log = Logger.getLogger(MvelRequestBinder.class.getName());

  private static final String VALID_BINDING_REGEX = "[\\w\\.$]*";

  @Inject
  public MvelRequestBinder(Evaluator evaluator, Provider<FlashCache> cacheProvider) {
    this.evaluator = evaluator;
    this.cacheProvider = cacheProvider;
  }

  public void bind(HttpServletRequest request, Object o) {
    @SuppressWarnings("unchecked")
    final Map<String, String[]> map = request.getParameterMap();

    //bind iteratively (last incoming param-value per key, gets bound)
    for (Map.Entry<String, String[]> entry : map.entrySet()) {
      String key = entry.getKey();

      // If there are multiple entry, then this is a collection bind:
      final String[] values = entry.getValue();

      validate(key);

      Object value;

      if (values.length > 1) {
        value = Lists.newArrayList(values);
      } else {
        // If there is only one value, bind as per normal
        String rawValue = values[0];   //choose first (and only value)

        //bind from collection?
        if (rawValue.startsWith(COLLECTION_BIND_PREFIX)) {
          final String[] binding = rawValue.substring(COLLECTION_BIND_PREFIX.length()).split("/");
          if (binding.length != 2)
            throw new InvalidBindingException(
                "Collection sources must be bound in the form '[C/collection/hashcode'. "
                    + "Was the request corrupt? Or did you try to bind something manually"
                    + " with a key starting '[C/'? Was: " + rawValue);

          final Collection<?> collection = cacheProvider.get().get(binding[0]);

          value = search(collection, binding[1]);
        } else
          value = rawValue;
      }

      //apply the bound value to the page object property
      try {
        evaluator.write(key, o, value);
      } catch (PropertyAccessException e) {

        // Do some better error reporting if this is a real exception.
        if (e.getCause() instanceof InvocationTargetException) {
          Throwable cause = e.getCause().getCause();
          StackTraceElement[] stackTrace = cause.getStackTrace();
          throw new RuntimeException(String.format(
              "Exception [%s - \"%s\"] thrown by setter [%s] for value[%s]\n\nat %s\n"
              + "(See below for entire trace.)\n",
              cause.getClass().getSimpleName(),
              cause.getMessage(), key, value,
              stackTrace[0]), e);
        }
        // Log missing property.
        if (log.isLoggable(Level.FINE)) {
          log.fine(String.format("A property [%s] could not be bound,"
              + " but not necessarily an error.", key));
        }
      }
    }
  }

  //TODO optimize this to be aggressive based on collection type
  //Linear collection search by hashcode
  private Object search(Collection<?> collection, String hashKey) {
    int hash = Integer.valueOf(hashKey);

    for (Object o : collection) {
      if (o.hashCode() == hash)
        return o;
    }

    //nothing found
    return null;
  }

  private void validate(String binding) {
    //guard against expression-injection attacks
    // TODO use an optimized algorithm, rather than a regex?
    if (Strings.empty(binding) || !binding.matches(VALID_BINDING_REGEX))
      throw new InvalidBindingException(
          "Binding expression (request/form parameter) contained invalid characters: " + binding);
  }
}

@@ -2683,20 +2683,18 @@
  %7B%0A%0A    
-    
+%09%09
 // Do so
@@ -2748,26 +2748,26 @@
 eption.%0A    
-  
+%09%09
   if (e.getC
@@ -2821,443 +2821,369 @@
     
-      Throwable cause = e.getCause().getCause();%0A          StackTraceElement%5B%5D stackTrace = cause.getStackTrace();%0A          throw new RuntimeException(String.format(%0A              %22Exception %5B%25s - %5C%22%25s%5C%22%5D thrown by setter %5B%25s%5D for value%5B%25s%5D%5Cn%5Cnat %25s%5Cn%22%0A              + %22(See below for entire trace.)%5Cn%22,%0A              cause.getClass().getSimpleName(),%0A              cause.getMessage(), key, value,%0A              stackTrace%5B0%5D)
+%09%09%09  addContextAndThrow(o, key, value, e.getCause());%0A    %09%09  %7D%0A        // Log missing property.%0A        if (log.isLoggable(Level.FINE)) %7B%0A          log.fine(String.format(%22A property %5B%25s%5D could not be bound,%22%0A              + %22 but not necessarily an error.%22, key));%0A        %7D%0A      %7D%0A      catch (Exception e) %7B%0A          addContextAndThrow(o, key, value
 , e);%0A
-  
     
@@ -3194,233 +3194,249 @@
     
-    // Log missing property.%0A        if (log.isLoggable(Level.FINE)) 
+%7D%0A  %7D%0A%0A%09private void addContextAndThrow(Object bound, String key, Object value, Throwable cause)%0A%09
 %7B%0A
+%09
   
-        log.fine(String.format(%22A property %5B%25s%5D could not be bound,%22%0A              + %22 but not necessarily an error.%22, key));%0A        %7D%0A      %7D%0A    %7D%0A  
+throw new RuntimeException(String.format(%0A%09    %22Problem setting %5B%25s%5D on instance %5B%25s%5D with value %5B%25s%5D%22,%0A%09    key, bound, value), cause);%0A%09
 %7D%0A%0A 

Add context to exceptions that are not wrapped in PropertyAccessException