package appengine.util;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang.builder.ToStringBuilder;

import com.google.appengine.api.labs.taskqueue.TaskQueuePb;
import com.google.appengine.api.memcache.MemcacheServicePb;
import com.google.appengine.repackaged.com.google.io.protocol.ProtocolMessage;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.DatastorePb;
import com.google.apphosting.api.ApiProxy.ApiProxyException;
import com.google.apphosting.api.ApiProxy.Delegate;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.apphosting.api.ApiProxy.LogRecord;

/**
 * LowLevelAPIでの各種サービスへのアクセスをTraceする{@link Delegate}の実装。
 * @author shin1ogawa
 */
public class TraceLowLevelDelegate implements Delegate<Environment> {

	static final Logger logger = Logger.getLogger(TraceLowLevelDelegate.class.getName());

	final Delegate<Environment> original;

	boolean traceOn = true;


	/**
	 * the constructor.
	 * @param original
	 * @category constructor
	 */
	public TraceLowLevelDelegate(Delegate<Environment> original) {
		this.original = original;
	}

	/**
	 * @param traceOn the traceOn to set
	 * @category accessor
	 */
	public void setTraceOn(boolean traceOn) {
		this.traceOn = traceOn;
	}

	/**
	 * @return the traceOn
	 * @category accessor
	 */
	public boolean isTraceOn() {
		return traceOn;
	}

	/**
	 * 低レベルAPIの呼び出しをトレースするための{@link TraceLowLevelDelegate}を挟む。
	 */
	public static void delegateToTraceLowLevel() {
		@SuppressWarnings("unchecked")
		Delegate<Environment> original = ApiProxy.getDelegate();
		ApiProxy.setDelegate(new TraceLowLevelDelegate(original));
		inspectOriginal(original);
	}

	private static void inspectOriginal(Delegate<Environment> original) {
		logger.log(Level.FINE, "original delegate class=" + original.getClass());
		logger.log(Level.FINE, ToStringBuilder.reflectionToString(original));
		Method[] methods = original.getClass().getMethods();
		if (methods != null) {
			for (Method method : methods) {
				if (!method.getDeclaringClass().equals(Object.class)) {
					logger.log(Level.FINE, method.toString());
				}
			}
		}
		methods = original.getClass().getDeclaredMethods();
		if (methods != null) {
			for (Method method : methods) {
				if (!method.getDeclaringClass().equals(Object.class)) {
					logger.log(Level.FINE, method.toString());
				}
			}
		}
		Field[] fields = original.getClass().getFields();
		if (fields != null) {
			for (Field field : fields) {
				if (!field.getDeclaringClass().equals(Object.class)) {
					logger.log(Level.FINE, field.toString());
				}
			}
		}
		fields = original.getClass().getDeclaredFields();
		if (fields != null) {
			for (Field field : fields) {
				if (!field.getDeclaringClass().equals(Object.class)) {
					logger.log(Level.FINE, field.toString());
				}
			}
		}
	}

	/**
	 * {@link TraceLowLevelDelegate}をはずす。
	 */
	public static void restoreDelegateFromTraceLowLevel() {
		if (ApiProxy.getDelegate() instanceof TraceLowLevelDelegate) {
			ApiProxy.setDelegate(((TraceLowLevelDelegate) ApiProxy.getDelegate()).getOriginal());
		}
	}

	/**
	 * 現在の{@link ApiProxy#getDelegate()}が{@link TraceLowLevelDelegate}だった場合、トレース出力を切り替える。
	 * @param onOrOff
	 */
	public static void setTrace(boolean onOrOff) {
		if (ApiProxy.getDelegate() instanceof TraceLowLevelDelegate) {
			((TraceLowLevelDelegate) ApiProxy.getDelegate()).setTraceOn(onOrOff);
		}
	}

	public byte[] makeSyncCall(Environment environment, String packageName, String methodName,
			byte[] request) throws ApiProxyException {
		if (traceOn) {
			logger.info("makeSyncCall: serviceName=" + packageName + ", methodName=" + methodName);
			traceRequest(packageName, methodName, request);
		}
		byte[] makeSyncCall =
				getOriginal().makeSyncCall(environment, packageName, methodName, request);
		return makeSyncCall;
	}

	private void traceRequest(String packageName, String methodName, byte[] request) {
		if (packageName.equals("datastore_v3")) {
			traceDatastoreServiceRequest(packageName, methodName, request);
		} else if (packageName.equals("memcache")) {
			traceMemcacheServiceRequest(packageName, methodName, request);
		} else if (packageName.equals("taskqueue")) {
			traceTaskQueueServiceRequest(packageName, methodName, request);
		}
	}

	/**
	 * @param packageName
	 * @param methodName
	 * @param request
	 * @see TaskQueuePb
	 */
	private void traceTaskQueueServiceRequest(String packageName, String methodName, byte[] request) {
		if (methodName.equalsIgnoreCase("Add")) {
			convertToPb(packageName, methodName, request, TaskQueuePb.TaskQueueAddRequest.class);
		}
	}

	/**
	 * @param packageName
	 * @param methodName
	 * @param request
	 * @see DatastorePb
	 */
	private void traceDatastoreServiceRequest(String packageName, String methodName, byte[] request) {
		if (methodName.equalsIgnoreCase("Put")) {
			convertToPb(packageName, methodName, request, DatastorePb.PutRequest.class);
		} else if (methodName.equalsIgnoreCase("Delete")) {
			convertToPb(packageName, methodName, request, DatastorePb.DeleteRequest.class);
		} else if (methodName.equalsIgnoreCase("Get")) {
			convertToPb(packageName, methodName, request, DatastorePb.GetRequest.class);
		} else if (methodName.equalsIgnoreCase("AllocateIds")) {
			convertToPb(packageName, methodName, request, DatastorePb.AllocateIdsRequest.class);
		} else if (methodName.equalsIgnoreCase("RunQuery")) {
			convertToPb(packageName, methodName, request, DatastorePb.Query.class);
		} else if (methodName.equalsIgnoreCase("Count")) {
			convertToPb(packageName, methodName, request, DatastorePb.Query.class);
		} else if (methodName.equalsIgnoreCase("BeginTransaction")) {
			// 解析がまだ完全ではない。
//			convertToPb(packageName, methodName, request, DatastorePb.Transaction.class);
		} else if (methodName.equalsIgnoreCase("Commit")) {
			// 解析がまだ完全ではない。
//			convertToPb(packageName, methodName, request, DatastorePb.Transaction.class);
		} else if (methodName.equalsIgnoreCase("Rollback")) {
			// 解析がまだ完全ではない。
//			convertToPb(packageName, methodName, request, DatastorePb.Transaction.class);
		}
	}

	/**
	 * @param packageName
	 * @param methodName
	 * @param request
	 * @see MemcacheServicePb
	 */
	private void traceMemcacheServiceRequest(String packageName, String methodName, byte[] request) {
		if (methodName.equalsIgnoreCase("Get")) {
			convertToPb(packageName, methodName, request,
					MemcacheServicePb.MemcacheGetRequest.class);
		} else if (methodName.equalsIgnoreCase("Set")) {
			convertToPb(packageName, methodName, request,
					MemcacheServicePb.MemcacheSetRequest.class);
		} else if (methodName.equalsIgnoreCase("Delete")) {
			convertToPb(packageName, methodName, request,
					MemcacheServicePb.MemcacheDeleteRequest.class);
		} else if (methodName.equalsIgnoreCase("FlushAll")) {
			convertToPb(packageName, methodName, request,
					MemcacheServicePb.MemcacheFlushRequest.class);
		} else if (methodName.equalsIgnoreCase("Increment")) {
			convertToPb(packageName, methodName, request,
					MemcacheServicePb.MemcacheIncrementRequest.class);
		} else if (methodName.equalsIgnoreCase("Stats")) {
			convertToPb(packageName, methodName, request,
					MemcacheServicePb.MemcacheStatsRequest.class);
		}
	}

	private void convertToPb(String packageName, String methodName, byte[] request,
			Class<?> convertedClass) {
		try {
			Object pb = convertBytesToPb(request, convertedClass);
			logger
				.log(Level.INFO, packageName + ":" + methodName + "のRequestを"
						+ convertedClass.getName() + "にconvertBytesToPbできた！変換結果のクラス="
						+ pb.getClass() + " ,protocolBuffer=\n{{{\n" + pb + "}}}"/*ToStringBuilder.reflectionToString(pb)*/);
		} catch (Exception ex) {
			logger.log(Level.WARNING, packageName + ":" + methodName + "のRequestを"
					+ convertedClass.getName() + "にconvertBytesToPbできませんでした。", ex);
		}
	}

	public void log(Environment environment, LogRecord logRecord) {
		getOriginal().log(environment, logRecord);
	}

	Delegate<Environment> getOriginal() {
		return original;
	}

	@SuppressWarnings("unchecked")
	Object convertBytesToPb(byte bytes[], Class requestClass) throws IllegalAccessException,
			InstantiationException, InvocationTargetException, NoSuchMethodException {
		if (com.google.appengine.repackaged.com.google.io.protocol.ProtocolMessage.class
			.isAssignableFrom(requestClass)) {
			ProtocolMessage proto = (ProtocolMessage) requestClass.newInstance();
			proto.mergeFrom(bytes);
			return proto;
		}
		if (com.google.appengine.repackaged.com.google.protobuf.Message.class
			.isAssignableFrom(requestClass)) {
			Method method = requestClass.getMethod("parseFrom", new Class[] {
				byte[].class
			});
			return method.invoke(null, new Object[] {
				bytes
			});
		} else {
			throw new UnsupportedOperationException((new StringBuilder()).append(
					"Cannot convert byte[] to ").append(requestClass).toString());
		}
	}
}
