/*********************************************************************
 * Copyright (c) 2010 Sony Ericsson/ST Ericsson and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 *  Contributors:
 *      Sony Ericsson/ST Ericsson - initial API and implementation
 *      Tasktop Technologies - improvements
 *      Jan Lohre (SAP) - improvements
 *********************************************************************/

package org.eclipse.mylyn.internal.gerrit.core.client;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.eclipse.jgit.diff.Edit;

import com.google.gerrit.common.data.GerritConfig;
import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.ApprovalCategory.Id;
import com.google.gerrit.reviewdb.AuthType;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.Project;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.google.gwtjsonrpc.server.JsonServlet;

/**
 * @author Steffen Pingel
 */
public class JSonSupport {

	/**
	 * Parses a Json response.
	 */
	private class JSonResponseDeserializer implements JsonDeserializer<JSonResponse> {
		public JSonResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
				throws JsonParseException {
			JsonObject object = json.getAsJsonObject();
			JSonResponse response = new JSonResponse();
			response.jsonrpc = object.get("jsonrpc").getAsString(); //$NON-NLS-1$
			response.id = object.get("id").getAsInt(); //$NON-NLS-1$
			response.result = object.get("result"); //$NON-NLS-1$
			response.error = object.get("error"); //$NON-NLS-1$			
			return response;
		}
	}

	static class JSonError {

		int code;

		String message;

	}

	static class JsonRequest {

		int id;

		final String jsonrpc = "2.0"; //$NON-NLS-1$

		String method;

		final List<Object> params = new ArrayList<Object>();

		String xsrfKey;
	}

	static class JSonResponse {

		JsonElement error;

		int id;

		String jsonrpc;

		JsonElement result;

	}

	private Gson gson;

	public JSonSupport() {
		TypeToken<Map<Id, PatchSetApproval>> approvalMapType = new TypeToken<Map<ApprovalCategory.Id, PatchSetApproval>>() {
		};
		ExclusionStrategy exclustionStrategy = new ExclusionStrategy() {

			public boolean shouldSkipField(FieldAttributes f) {
				// commentLinks requires instantiation of com.google.gwtexpui.safehtml.client.RegexFindReplace which is not on classpath
				if (f.getDeclaredClass() == List.class
						&& f.getName().equals("commentLinks") && f.getDeclaringClass() == GerritConfig.class) { //$NON-NLS-1$
					return true;
				}
				if (f.getDeclaredClass() == Map.class && f.getName().equals("given")) { //$NON-NLS-1$
					//return true;
				}
				// GSon 2.1 fails to deserialize the SubmitType enum
				if (f.getDeclaringClass() == Project.class && f.getName().equals("submitType")) { //$NON-NLS-1$
					return true;
				}
				return false;
			}

			public boolean shouldSkipClass(Class<?> clazz) {
				return false;
			}
		};
		gson = JsonServlet.defaultGsonBuilder()
				.registerTypeAdapter(JSonResponse.class, new JSonResponseDeserializer())
				.registerTypeAdapter(Edit.class, new JsonDeserializer<Edit>() {
					public Edit deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
							throws JsonParseException {
						if (json.isJsonArray()) {
							JsonArray array = json.getAsJsonArray();
							if (array.size() == 4) {
								return new Edit(array.get(0).getAsInt(), array.get(1).getAsInt(), array.get(2)
										.getAsInt(), array.get(3).getAsInt());
							}
						}
						return new Edit(0, 0);
					}
				})
				// ignore GerritForge specific AuthType "TEAMFORGE" which is unknown to Gerrit
				.registerTypeAdapter(AuthType.class, new JsonDeserializer<AuthType>() {

					@Override
					public AuthType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
							throws JsonParseException {
						String jsonString = json.getAsString();
						if (jsonString != null) {
							try {
								return AuthType.valueOf(jsonString);
							} catch (IllegalArgumentException e) {
								// ignore the error since the connector does not make use of AuthType
								//GerritCorePlugin.logWarning("Ignoring unkown authentication type: " + jsonString, e);
							}
						}
						return null;
					}
				})
				.registerTypeAdapter(approvalMapType.getType(), new JsonDeserializer<Map<Id, PatchSetApproval>>() {

					@Override
					public Map<Id, PatchSetApproval> deserialize(JsonElement json, Type typeOfT,
							JsonDeserializationContext context) throws JsonParseException {
						// Gerrit 2.2: the type of PatchSetPublishDetail.given changed from a map to a list
						Map<Id, PatchSetApproval> map = new HashMap<ApprovalCategory.Id, PatchSetApproval>();
						if (json.isJsonArray()) {
							JsonArray array = json.getAsJsonArray();
							for (Iterator<JsonElement> it = array.iterator(); it.hasNext();) {
								JsonElement element = it.next();
								Id key = context.deserialize(element, Id.class);
								if (key.get() != null) {
									// Gerrit < 2.1.x: json is map
									element = it.next();
								}
								PatchSetApproval value = context.deserialize(element, PatchSetApproval.class);
								if (key.get() == null) {
									// Gerrit 2.2: json is a list, deduct key from value
									key = value.getCategoryId();
								}
								map.put(key, value);
							}
						}
						return map;
					}
				})
				.setExclusionStrategies(exclustionStrategy)
				.create();
	}

	String createRequest(int id, String xsrfKey, String methodName, Collection<Object> args) {
		JsonRequest msg = new JsonRequest();
		msg.method = methodName;
		if (args != null) {
			for (Object arg : args) {
				msg.params.add(arg);
			}
		}
		msg.id = id;
		msg.xsrfKey = xsrfKey;
		return gson.toJson(msg, msg.getClass());
	}

	<T> T parseJsonResponse(String responseMessage, Type resultType) throws GerritException {
		JSonResponse response = parseResponse(responseMessage, JSonResponse.class);
		if (response.error != null) {
			JSonError error = gson.fromJson(response.error, JSonError.class);
			throw new GerritException(error.message, error.code);
		} else {
			return gson.fromJson(response.result, resultType);
		}
	}

	public <T> T parseResponse(String responseMessage, Type resultType) {
		Assert.isLegal(responseMessage != null);
		Assert.isLegal(!responseMessage.isEmpty());

		// Gerrit 2.5 prepends the output with bogus characters
		// see http://code.google.com/p/gerrit/issues/detail?id=1648
		if (responseMessage.startsWith(")]}'\n")) { //$NON-NLS-1$
			responseMessage = responseMessage.substring(5);
		}
		return gson.fromJson(responseMessage, resultType);
	}

	public String toJson(Object src) {
		return gson.toJson(src);
	}
}
