/*
 * DISCLAIMER
 *
 * Copyright 2017 ArangoDB GmbH, Cologne, Germany
 *
 * 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.
 *
 * Copyright holder is ArangoDB GmbH, Cologne, Germany
 */

package com.arangodb.springframework.core.convert.resolver;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.ReflectionUtils;

/**
 * @author Mark Vollmary
 * @author Christian Lechner
 *
 */
public abstract class AbstractResolver<A extends Annotation> {

	private static final Method GET_ENTITY_METHOD;
	private static final Method GET_REF_ID_METHOD;

	static {
		try {
			GET_ENTITY_METHOD = LazyLoadingProxy.class.getMethod("getEntity");
			GET_REF_ID_METHOD = LazyLoadingProxy.class.getMethod("getRefId");
		} catch (final Exception e) {
			throw new RuntimeException(e);
		}
	}

	private final ObjenesisStd objenesis;
	private final ConversionService conversionService;

	protected AbstractResolver(final ConversionService conversionService) {
		super();
		this.conversionService = conversionService;
		this.objenesis = new ObjenesisStd(true);
	}

	static interface ResolverCallback<A extends Annotation> {

		Object resolve(String id, TypeInformation<?> type, A annotation);

	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	protected Object proxy(
		final String id,
		final TypeInformation<?> type,
		final A annotation,
		final ResolverCallback<A> callback) {
		final ProxyInterceptor interceptor = new ProxyInterceptor(id, type, annotation, callback, conversionService);
		if (type.getType().isInterface()) {
			final ProxyFactory proxyFactory = new ProxyFactory(new Class<?>[] { type.getType() });
			for (final Class<?> interf : type.getType().getInterfaces()) {
				proxyFactory.addInterface(interf);
			}
			proxyFactory.addInterface(LazyLoadingProxy.class);
			proxyFactory.addAdvice(interceptor);
			return proxyFactory.getProxy();
		} else {
			final Factory factory = (Factory) objenesis.newInstance(enhancedTypeFor(type.getType()));
			factory.setCallbacks(new Callback[] { interceptor });
			return factory;
		}
	}

	private Class<?> enhancedTypeFor(final Class<?> type) {
		final Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(type);
		enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
		enhancer.setInterfaces(new Class[] { LazyLoadingProxy.class });
		return enhancer.createClass();
	}

	static class ProxyInterceptor<A extends Annotation> implements Serializable,
			org.springframework.cglib.proxy.MethodInterceptor, org.aopalliance.intercept.MethodInterceptor {

		private static final long serialVersionUID = -6722757823918987065L;
		private final String id;
		final TypeInformation<?> type;
		private final A annotation;
		private final ResolverCallback<A> callback;
		private volatile boolean resolved;
		private Object result;
		private final ConversionService conversionService;

		public ProxyInterceptor(final String id, final TypeInformation<?> type, final A annotation,
			final ResolverCallback<A> callback, final ConversionService conversionService) {
			super();
			this.id = id;
			this.type = type;
			this.annotation = annotation;
			this.callback = callback;
			this.conversionService = conversionService;
			result = null;
			resolved = false;
		}

		@Override
		public Object invoke(final MethodInvocation invocation) throws Throwable {
			return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null);
		}

		@Override
		public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy)
				throws Throwable {

			if (GET_ENTITY_METHOD.equals(method)) {
				return ensureResolved();
			}

			if (GET_REF_ID_METHOD.equals(method)) {
				return id;
			}

			if (ReflectionUtils.isObjectMethod(method)) {
				if (ReflectionUtils.isToStringMethod(method)) {
					return proxyToString();
				}

				else if (ReflectionUtils.isEqualsMethod(method)) {
					return proxyEquals(proxy, args[0]);
				}

				else if (ReflectionUtils.isHashCodeMethod(method)) {
					return proxyHashCode();
				}
			}

			final Object result = ensureResolved();
			return result == null ? null : method.invoke(result, args);
		}

		private Object ensureResolved() {
			if (!resolved) {
				result = resolve();
				resolved = true;
			}
			return result;
		}

		private synchronized Object resolve() {
			if (!resolved) {
				return convertIfNecessary(callback.resolve(id, type, annotation), type.getType());
			}
			return result;
		}

		private String proxyToString() {
			final StringBuilder str = new StringBuilder();
			str.append(LazyLoadingProxy.class.getSimpleName());
			str.append(" [");
			str.append(id);
			str.append("]");
			return str.toString();
		}

		private int proxyHashCode() {
			return proxyToString().hashCode();
		}

		private boolean proxyEquals(final Object proxy, final Object obj) {
			if (!(obj instanceof LazyLoadingProxy)) {
				return false;
			}
			if (obj == proxy) {
				return true;
			}
			return proxyToString().equals(obj.toString());
		}

		@SuppressWarnings("unchecked")
		private <T> T convertIfNecessary(@Nullable final Object source, final Class<T> type) {
			return (T) (source == null ? null
					: type.isAssignableFrom(source.getClass()) ? source : conversionService.convert(source, type));
		}
	}

	protected static TypeInformation<?> getNonNullComponentType(final TypeInformation<?> type) {
		final TypeInformation<?> compType = type.getComponentType();
		return compType != null ? compType : ClassTypeInformation.OBJECT;
	}

}