/*
 * 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.requestfactory.server;

import com.google.web.bindery.requestfactory.shared.BaseProxy;
import com.google.web.bindery.requestfactory.shared.Locator;
import com.google.web.bindery.requestfactory.shared.RequestContext;
import com.google.web.bindery.requestfactory.shared.RequestFactory;
import com.google.web.bindery.requestfactory.shared.ServiceLocator;

import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * A cache for idempotent methods in {@link ServiceLayer}. The caching is
 * separate from {@link ReflectiveServiceLayer} so that the cache can be applied
 * to any decorators injected by the user.
 */
class ServiceLayerCache extends ServiceLayerDecorator {

  /**
   * ConcurrentHashMaps don't allow null keys or values, but sometimes we want
   * to cache a null value.
   */
  private static final Object NULL_MARKER = new Object();

  private static SoftReference<Map<Method, Map<Object, Object>>> methodCache;

  private static final Method createLocator;
  private static final Method createServiceInstance;
  private static final Method getDomainClassLoader;
  private static final Method getGetter;
  private static final Method getIdType;
  private static final Method getRequestReturnType;
  private static final Method getSetter;
  private static final Method requiresServiceLocator;
  private static final Method resolveClass;
  private static final Method resolveClientType;
  private static final Method resolveDomainClass;
  private static final Method resolveDomainMethod;
  private static final Method resolveLocator;
  private static final Method resolveRequestContext;
  private static final Method resolveRequestContextMethod;
  private static final Method resolveRequestFactory;
  private static final Method resolveServiceClass;
  private static final Method resolveServiceLocator;
  private static final Method resolveTypeToken;

  static {
    createLocator = getMethod("createLocator", Class.class);
    createServiceInstance = getMethod("createServiceInstance", Class.class);
    getDomainClassLoader = getMethod("getDomainClassLoader");
    getGetter = getMethod("getGetter", Class.class, String.class);
    getIdType = getMethod("getIdType", Class.class);
    getRequestReturnType = getMethod("getRequestReturnType", Method.class);
    getSetter = getMethod("getSetter", Class.class, String.class);
    requiresServiceLocator = getMethod("requiresServiceLocator", Method.class, Method.class);
    resolveClass = getMethod("resolveClass", String.class);
    resolveClientType = getMethod("resolveClientType", Class.class, Class.class, boolean.class);
    resolveDomainClass = getMethod("resolveDomainClass", Class.class);
    resolveDomainMethod = getMethod("resolveDomainMethod", String.class);
    resolveLocator = getMethod("resolveLocator", Class.class);
    resolveRequestContext = getMethod("resolveRequestContext", String.class);
    resolveRequestContextMethod = getMethod("resolveRequestContextMethod", String.class);
    resolveRequestFactory = getMethod("resolveRequestFactory", String.class);
    resolveServiceClass = getMethod("resolveServiceClass", Class.class);
    resolveServiceLocator = getMethod("resolveServiceLocator", Class.class);
    resolveTypeToken = getMethod("resolveTypeToken", Class.class);
  }

  private static Map<Method, Map<Object, Object>> getCache() {
    Map<Method, Map<Object, Object>> toReturn = methodCache == null ? null : methodCache.get();
    if (toReturn == null) {
      toReturn = new ConcurrentHashMap<Method, Map<Object, Object>>();
      methodCache = new SoftReference<Map<Method, Map<Object, Object>>>(toReturn);
    }
    return toReturn;
  }

  private static Method getMethod(String name, Class<?>... argTypes) {
    try {
      return ServiceLayer.class.getMethod(name, argTypes);
    } catch (SecurityException e) {
      throw new RuntimeException("Could not set up ServiceLayerCache Methods", e);
    } catch (NoSuchMethodException e) {
      throw new RuntimeException("Could not set up ServiceLayerCache Methods", e);
    }
  }

  private final Map<Method, Map<Object, Object>> methodMap = getCache();

  @Override
  public <T extends Locator<?, ?>> T createLocator(Class<T> clazz) {
    return getOrCache(createLocator, clazz, clazz, clazz);
  }

  @Override
  public Object createServiceInstance(Class<? extends RequestContext> requestContext) {
    return getOrCache(createServiceInstance, requestContext, Object.class, requestContext);
  }

  @Override
  public ClassLoader getDomainClassLoader() {
    return getOrCache(getDomainClassLoader, NULL_MARKER, ClassLoader.class);
  }

  @Override
  public Method getGetter(Class<?> domainType, String property) {
    return getOrCache(getGetter, new Pair<Class<?>, String>(domainType, property), Method.class,
        domainType, property);
  }

  @Override
  public Class<?> getIdType(Class<?> domainType) {
    return getOrCache(getIdType, domainType, Class.class, domainType);
  }

  @Override
  public Type getRequestReturnType(Method contextMethod) {
    return getOrCache(getRequestReturnType, contextMethod, Type.class, contextMethod);
  }

  @Override
  public Method getSetter(Class<?> domainType, String property) {
    return getOrCache(getSetter, new Pair<Class<?>, String>(domainType, property), Method.class,
        domainType, property);
  }

  @Override
  public boolean requiresServiceLocator(Method contextMethod, Method domainMethod) {
    return getOrCache(requiresServiceLocator,
        new Pair<Method, Method>(contextMethod, domainMethod), Boolean.class, contextMethod,
        domainMethod);
  }

  @Override
  public Class<? extends BaseProxy> resolveClass(String typeToken) {
    Class<?> found = getOrCache(resolveClass, typeToken, Class.class, typeToken);
    return found.asSubclass(BaseProxy.class);
  }

  @Override
  public <T> Class<? extends T> resolveClientType(Class<?> domainClass, Class<T> clientType,
      boolean required) {
    Class<?> clazz =
        getOrCache(resolveClientType, new Pair<Class<?>, Class<?>>(domainClass, clientType),
            Class.class, domainClass, clientType, required);
    return clazz == null ? null : clazz.asSubclass(clientType);
  }

  @Override
  public Class<?> resolveDomainClass(Class<?> clazz) {
    return getOrCache(resolveDomainClass, clazz, Class.class, clazz);
  }

  @Override
  public Method resolveDomainMethod(String operation) {
    return getOrCache(resolveDomainMethod, operation, Method.class, operation);
  }

  @Override
  @SuppressWarnings("unchecked")
  public Class<? extends Locator<?, ?>> resolveLocator(Class<?> domainType) {
    return getOrCache(resolveLocator, domainType, Class.class, domainType);
  }

  @Override
  public Class<? extends RequestContext> resolveRequestContext(String operation) {
    Class<?> clazz = getOrCache(resolveRequestContext, operation, Class.class, operation);
    return clazz.asSubclass(RequestContext.class);
  }

  @Override
  public Method resolveRequestContextMethod(String operation) {
    return getOrCache(resolveRequestContextMethod, operation, Method.class, operation);
  }

  @Override
  public Class<? extends RequestFactory> resolveRequestFactory(String binaryName) {
    Class<?> clazz = getOrCache(resolveRequestFactory, binaryName, Class.class, binaryName);
    return clazz.asSubclass(RequestFactory.class);
  }

  @Override
  public Class<?> resolveServiceClass(Class<? extends RequestContext> requestContextClass) {
    return getOrCache(resolveServiceClass, requestContextClass, Class.class, requestContextClass);
  }

  @Override
  public Class<? extends ServiceLocator> resolveServiceLocator(
      Class<? extends RequestContext> requestContext) {
    Class<?> clazz = getOrCache(resolveServiceLocator, requestContext, Class.class, requestContext);
    return clazz == null ? null : clazz.asSubclass(ServiceLocator.class);
  }

  @Override
  public String resolveTypeToken(Class<? extends BaseProxy> domainClass) {
    return getOrCache(resolveTypeToken, domainClass, String.class, domainClass);
  }

  private <K, T> T getOrCache(Method method, K key, Class<T> valueType, Object... args) {
    Map<Object, Object> map = methodMap.get(method);
    if (map == null) {
      map = new ConcurrentHashMap<Object, Object>();
      methodMap.put(method, map);
    }
    Object raw = map.get(key);
    if (raw == NULL_MARKER) {
      return null;
    }
    T toReturn = valueType.cast(raw);
    if (toReturn == null) {
      Throwable ex = null;
      try {
        toReturn = valueType.cast(method.invoke(getNext(), args));
        map.put(key, toReturn == null ? NULL_MARKER : toReturn);
      } catch (InvocationTargetException e) {
        // The next layer threw an exception
        Throwable cause = e.getCause();
        if (cause instanceof RuntimeException) {
          // Re-throw RuntimeExceptions, which likely originate from die()
          throw ((RuntimeException) cause);
        }
        die(cause, "Unexpected checked exception");
      } catch (IllegalArgumentException e) {
        ex = e;
      } catch (IllegalAccessException e) {
        ex = e;
      }
      if (ex != null) {
        die(ex, "Bad method invocation");
      }
    }
    return toReturn;
  }
}
