/*
 * Copyright 2010 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.web.bindery.autobean.vm.impl;

import com.google.gwt.core.client.impl.WeakMapping;
import com.google.web.bindery.autobean.shared.AutoBean;
import com.google.web.bindery.autobean.shared.AutoBeanFactory;
import com.google.web.bindery.autobean.shared.AutoBeanUtils;
import com.google.web.bindery.autobean.shared.AutoBeanVisitor;
import com.google.web.bindery.autobean.shared.impl.AbstractAutoBean;
import com.google.web.bindery.autobean.vm.Configuration;

import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * An implementation of an AutoBean that uses reflection.
 * 
 * @param <T> the type of interface being wrapped
 */
public class ProxyAutoBean<T> extends AbstractAutoBean<T> {
  private static class Data {
    final Class<?> elementType;
    final Type genericType;
    final Method getter;
    final Class<?> keyType;
    final PropertyType propertyType;
    Method setter;
    final Class<?> type;
    final Class<?> valueType;

    Data(Method getter, Type genericType, Class<?> type, PropertyType propertyType) {
      this.getter = getter;
      this.genericType = genericType;
      this.type = type;
      this.propertyType = propertyType;

      if (propertyType == PropertyType.COLLECTION) {
        elementType =
            TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(Collection.class,
                genericType, type));
        keyType = valueType = null;
      } else if (propertyType == PropertyType.MAP) {
        elementType = null;
        Type[] types = TypeUtils.getParameterization(Map.class, genericType, type);
        keyType = TypeUtils.ensureBaseType(types[0]);
        valueType = TypeUtils.ensureBaseType(types[1]);
      } else {
        elementType = keyType = valueType = null;
      }
    }
  }

  private enum PropertyType {
    VALUE, REFERENCE, COLLECTION, MAP;
  }

  private static final Map<Class<?>, Map<String, Data>> cache =
      new WeakHashMap<Class<?>, Map<String, Data>>();

  /**
   * Utility method to crete a new {@link Proxy} instance.
   * 
   * @param <T> the interface type to be implemented by the Proxy
   * @param intf the Class representing the interface type
   * @param handler the implementation of the interface
   * @param extraInterfaces additional interface types the Proxy should
   *          implement
   * @return a Proxy instance
   */
  public static <T> T makeProxy(Class<T> intf, InvocationHandler handler,
      Class<?>... extraInterfaces) {
    Class<?>[] intfs;
    if (extraInterfaces == null) {
      intfs = new Class<?>[] {intf};
    } else {
      intfs = new Class<?>[extraInterfaces.length + 1];
      intfs[0] = intf;
      System.arraycopy(extraInterfaces, 0, intfs, 1, extraInterfaces.length);
    }

    return intf.cast(Proxy.newProxyInstance(intf.getClassLoader(), intfs, handler));
  }

  private static Map<String, Data> calculateData(Class<?> beanType) {
    Map<String, Data> toReturn;
    synchronized (cache) {
      toReturn = cache.get(beanType);
      if (toReturn == null) {
        Map<String, Data> getters = new HashMap<String, Data>();
        List<Method> setters = new ArrayList<Method>();
        if (!(Collection.class.isAssignableFrom(beanType)
            || Map.class.isAssignableFrom(beanType))) {
          for (Method method : beanType.getMethods()) {
            if (BeanMethod.GET.matches(method)) {
              // match methods on their name for now, to find the most specific
              // override
              String name = method.getName();

              Type genericReturnType = TypeUtils.resolveGenerics(beanType,
                  method.getGenericReturnType());
              Class<?> returnType = TypeUtils.ensureBaseType(genericReturnType);

              Data data = getters.get(name);
              if (data == null || data.type.isAssignableFrom(returnType)) {
                // no getter seen yet for the property, or a less specific one
                PropertyType propertyType;
                if (TypeUtils.isValueType(returnType)) {
                  propertyType = PropertyType.VALUE;
                } else if (Collection.class.isAssignableFrom(returnType)) {
                  propertyType = PropertyType.COLLECTION;
                } else if (Map.class.isAssignableFrom(returnType)) {
                  propertyType = PropertyType.MAP;
                } else {
                  propertyType = PropertyType.REFERENCE;
                }
                data = new Data(method, genericReturnType, returnType, propertyType);

                getters.put(name, data);
              }
            } else if (BeanMethod.SET.matches(method) || BeanMethod.SET_BUILDER.matches(method)) {
              setters.add(method);
            }
          }
        }

        toReturn = new HashMap<String, Data>(getters.size());

        // Now take @PropertyName into account
        for (Map.Entry<String, Data> entry : getters.entrySet()) {
          Data data = entry.getValue();
          toReturn.put(BeanMethod.GET.inferName(data.getter), data);
        }

        // Associate setters to getters
        for (Method setter : setters) {
          String name = BeanMethod.SET.inferName(setter);
          Data data = toReturn.get(name);
          if (data != null && data.setter == null
              && data.getter.getReturnType().isAssignableFrom(setter.getParameterTypes()[0])) {
            data.setter = setter;
          }
        }

        cache.put(beanType, toReturn);
      }
    }
    return toReturn;
  }

  private final Class<T> beanType;
  private final Configuration configuration;
  private final Map<String, Data> propertyData;
  /**
   * Because the shim and the ProxyAutoBean are related through WeakMapping, we
   * need to ensure that the ProxyAutoBean doesn't artificially extend the
   * lifetime of the shim. If there are no external references to the shim, it's
   * ok if it's deallocated, since it has no interesting state.
   * 
   * <pre>
   * _________________            ______________
   * | ProxyAutoBean |            |    Shim    |
   * |               | <----------+-bean       |
   * |          shim-+---X------> |            |
   * |_______________|            |____________|
   *         ^                           ^  ^
   *         X                           X  |
   *         |__value__WeakMapping__key__|  |
   *                      ^                 |
   *                      |                 |
   *                   GC Roots -> Owner____|
   * </pre>
   * <p>
   * In the case of a wrapped object (for example, an ArrayList), the weak
   * reference from WeakMapping to the ProxyAutoBean may cause the AutoBean to
   * be prematurely collected if neither the bean nor the shim are referenced
   * elsewhere. The alternative is a massive memory leak.
   */
  private WeakReference<T> shim;

  // These constructors mirror the generated constructors.
  @SuppressWarnings("unchecked")
  public ProxyAutoBean(AutoBeanFactory factory, Class<?> beanType, Configuration configuration) {
    super(factory);
    this.beanType = (Class<T>) beanType;
    this.configuration = configuration;
    this.propertyData = calculateData(beanType);
  }

  @SuppressWarnings("unchecked")
  public ProxyAutoBean(AutoBeanFactory factory, Class<?> beanType, Configuration configuration,
      T toWrap) {
    super(toWrap, factory);
    this.beanType = (Class<T>) beanType;
    this.configuration = configuration;
    this.propertyData = calculateData(beanType);
  }

  @Override
  public T as() {
    T toReturn = shim == null ? null : shim.get();
    if (toReturn == null) {
      toReturn = createShim();
      shim = new WeakReference<T>(toReturn);
    }
    return toReturn;
  }

  public Configuration getConfiguration() {
    return configuration;
  }

  public Class<T> getType() {
    return beanType;
  }

  /**
   * Allow access by {@link ShimHandler}.
   */
  @Override
  protected void call(String method, Object returned, Object... parameters) {
    super.call(method, returned, parameters);
  }

  /**
   * Allow access by {@link ShimHandler}.
   */
  @Override
  protected void checkFrozen() {
    super.checkFrozen();
  }

  /**
   * Allow access by {@link ShimHandler}.
   */
  @Override
  protected void checkWrapped() {
    super.checkWrapped();
  }

  /**
   * Not used in this implementation. Instead, the simple implementation is
   * created lazily in {@link #getWrapped()}.
   */
  @Override
  protected T createSimplePeer() {
    return null;
  }

  /**
   * Allow access by {@link ShimHandler}.
   */
  @Override
  protected <V> V get(String method, V toReturn) {
    return super.get(method, toReturn);
  }

  /**
   * Allow access by BeanMethod.
   */
  @Override
  protected <V> V getOrReify(String propertyName) {
    return super.<V> getOrReify(propertyName);
  }

  /**
   * Allow access by {@link ShimHandler}.
   */
  @Override
  protected T getWrapped() {
    if (wrapped == null && isUsingSimplePeer()) {
      wrapped = ProxyAutoBean.<T> makeProxy(beanType, new SimpleBeanHandler<T>(this));
    }
    return super.getWrapped();
  }

  /**
   * Allow access by {@link ShimHandler}.
   */
  @Override
  protected void set(String method, Object value) {
    super.set(method, value);
  }

  @Override
  protected void setProperty(String propertyName, Object value) {
    super.setProperty(propertyName, value);
  }

  // TODO: Port to model-based when class-based TypeOracle is available.
  @Override
  protected void traverseProperties(AutoBeanVisitor visitor, OneShotContext ctx) {
    for (Map.Entry<String, Data> entry : propertyData.entrySet()) {
      String name = entry.getKey();
      Data data = entry.getValue();
      Method getter = data.getter;
      PropertyType propertyType = data.propertyType;

      // Use the shim to handle automatic wrapping
      Object value;
      try {
        getter.setAccessible(true);
        value = getter.invoke(as());
      } catch (IllegalArgumentException e) {
        throw new RuntimeException(e);
      } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
      } catch (InvocationTargetException e) {
        throw new RuntimeException(e.getCause());
      }

      // Create the context used for the property visitation
      MethodPropertyContext x;
      if (isUsingSimplePeer()) {
        x =
            new BeanPropertyContext(this, name, data.genericType, data.type, data.elementType,
                data.keyType, data.valueType);
      } else {
        x =
            new GetterPropertyContext(this, getter, data.genericType, data.type, data.elementType,
                data.keyType, data.valueType);
      }

      switch (propertyType) {
        case VALUE: {
          if (visitor.visitValueProperty(name, value, x)) {
          }
          visitor.endVisitValueProperty(name, value, x);
          break;
        }
        case COLLECTION: {
          // Workaround for generics bug in mac javac 1.6.0_22
          @SuppressWarnings("rawtypes")
          AutoBean temp = AutoBeanUtils.getAutoBean((Collection) value);
          @SuppressWarnings("unchecked")
          AutoBean<Collection<?>> bean = (AutoBean<Collection<?>>) temp;
          if (visitor.visitCollectionProperty(name, bean, x)) {
            if (value != null) {
              ((ProxyAutoBean<?>) bean).traverse(visitor, ctx);
            }
          }
          visitor.endVisitCollectionProperty(name, bean, x);
          break;
        }
        case MAP: {
          // Workaround for generics bug in mac javac 1.6.0_22
          @SuppressWarnings("rawtypes")
          AutoBean temp = AutoBeanUtils.getAutoBean((Map) value);
          @SuppressWarnings("unchecked")
          AutoBean<Map<?, ?>> bean = (AutoBean<Map<?, ?>>) temp;
          if (visitor.visitMapProperty(name, bean, x)) {
            if (value != null) {
              ((ProxyAutoBean<?>) bean).traverse(visitor, ctx);
            }
          }
          visitor.endVisitMapProperty(name, bean, x);
          break;
        }
        case REFERENCE: {
          ProxyAutoBean<?> bean = (ProxyAutoBean<?>) AutoBeanUtils.getAutoBean(value);
          if (visitor.visitReferenceProperty(name, bean, x)) {
            if (value != null) {
              bean.traverse(visitor, ctx);
            }
          }
          visitor.endVisitReferenceProperty(name, bean, x);
          break;
        }
      }
    }
  }

  private T createShim() {
    T toReturn = ProxyAutoBean.makeProxy(beanType, new ShimHandler<T>(this, getWrapped()));
    WeakMapping.setWeak(toReturn, AutoBean.class.getName(), this);
    return toReturn;
  }
}
