/**
 * This file is part of pwt.
 *
 * pwt is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * pwt is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with pwt. If not,
 * see <http://www.gnu.org/licenses/>.
 */
package fr.putnami.pwt.core.service.client;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.RpcRequestBuilder;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamFactory;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.google.gwt.user.client.rpc.StatusCodeException;
import com.google.gwt.user.client.rpc.impl.RequestCallbackAdapter;
import com.google.gwt.user.client.rpc.impl.RequestCallbackAdapter.ResponseReader;
import com.google.gwt.user.client.rpc.impl.RpcStatsContext;
import com.google.gwt.user.client.rpc.impl.Serializer;
import com.google.web.bindery.event.shared.HandlerRegistration;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Stack;

import fr.putnami.pwt.core.error.client.ErrorManager;
import fr.putnami.pwt.core.event.client.EventBus;
import fr.putnami.pwt.core.service.client.error.ClientErrorHandler;
import fr.putnami.pwt.core.service.client.error.DefaultCommandExceptionErrorHandler;
import fr.putnami.pwt.core.service.client.error.ServerErrorHandler;
import fr.putnami.pwt.core.service.client.event.CommandRequestEvent;
import fr.putnami.pwt.core.service.client.event.CommandResponseEvent;
import fr.putnami.pwt.core.service.shared.domain.CommandDefinition;
import fr.putnami.pwt.core.service.shared.domain.CommandRequest;
import fr.putnami.pwt.core.service.shared.domain.CommandResponse;
import fr.putnami.pwt.core.service.shared.exception.CommandException;
import fr.putnami.pwt.core.service.shared.service.CommandService;

public final class DefaultCommandController extends CommandController {

	private class ServiceCallback extends CallbackAdapter<List<CommandResponse>> {

		private final List<Request> requests;
		private AsyncCallback<List<CommandResponse>> callback;

		ServiceCallback(List<Request> requests, AsyncCallback<List<CommandResponse>> callback) {
			this.requests = requests;
			this.callback = callback;
		}

		@Override
		public void onSuccess(List<CommandResponse> responses) {
			for (CommandResponse response : responses) {
				for (Request request : this.requests) {
					if (request.requestId == response.getRequestId()) {
						if (!request.param.isQuiet()) {
							DefaultCommandController.this.fireEvent(new CommandResponseEvent(request.requestId, request.command,
								response));
						}
						if (response.getThrown() == null) {
							for (AsyncCallback requestCallback : request.param.getCallbacks()) {
								if (response.getResult().size() == 1) {
									requestCallback.onSuccess(response.getResult().get(0));
								} else {
									requestCallback.onSuccess(null);
								}
							}
						} else {
							boolean caught = false;
							for (AsyncCallback requestCallback : request.param.getCallbacks()) {
								try {
									requestCallback.onFailure(response.getThrown());
									caught = true;
								} catch (RuntimeException e) {
									// Exception not handled.
									continue;
								}
							}
							if (!caught) {
								GWT.reportUncaughtException(response.getThrown());
							}
						}
					}
				}
			}
			if (this.callback != null) {
				this.callback.onSuccess(responses);
			}
		}

		@Override
		public void onFailure(Throwable caught) {
			if (caught instanceof StatusCodeException) {
				GWT.reportUncaughtException(caught);
			} else {
				for (Request request : this.requests) {
					if (request.param.getCallbacks().isEmpty()) {
						GWT.reportUncaughtException(caught);
					}
					for (AsyncCallback requestCallback : request.param.getCallbacks()) {
						requestCallback.onFailure(caught);
					}
				}
				if (this.callback != null) {
					this.callback.onFailure(caught);
				}
			}
		}
	}

	private static class Request {

		private long requestId;
		private CommandRequest command;
		private CommandParam param;
	}

	private static final String RPC_CONTENT_TYPE = "text/x-gwt-rpc; charset=utf-8";
	private static final String REMOTE_SERVICE_INTERFACE_NAME = CommandService.class.getName();
	private static final String METHOD_NAME = "executeCommands";

	private static DefaultCommandController instance;

	private final RpcRequestBuilder rpcRequestBuilder = new RpcRequestBuilder();
	private final String moduleBaseURL;
	private final String remoteServiceURL;

	private long requestIdSequence = 0;

	private boolean suspended = false;
	private Stack<Request> stack = new Stack<Request>();

	private DefaultCommandController() {
		this.moduleBaseURL = GWT.getHostPageBaseURL();
		this.remoteServiceURL = this.moduleBaseURL + "commandService";
		ErrorManager.get().registerErrorHandlers(new ClientErrorHandler(), new ServerErrorHandler(),
			new DefaultCommandExceptionErrorHandler());
	}

	@Override
	public CommandRequest invokeCommand(CommandDefinition commandDefinition, CommandParam commandParam) {

		long requestId = ++this.requestIdSequence;
		CommandRequest command = new CommandRequest();
		command.setRequestId(requestId);
		command.setCommandDefinition(commandDefinition);
		command.setArgs(commandParam.getParams());

		Request request = new Request();
		request.requestId = requestId;
		request.param = commandParam;
		request.command = command;

		if (this.suspended || request.param.isLazy()) {
			this.stack.push(request);
		} else {
			this.sendRequest(Lists.newArrayList(request), null);
		}

		return command;
	}

	@Override
	public int flush() {
		return this.flush(null);
	}

	@Override
	public int flush(AsyncCallback<List<CommandResponse>> callback) {
		try {
			int result = this.sendRequest(Lists.newArrayList(this.stack), callback);
			if (result == 0 && callback != null) {
				callback.onSuccess(Collections.<CommandResponse> emptyList());
			}
			return result;
		} finally {
			this.stack.clear();
		}
	}

	@Override
	public int countPendingRequest() {
		return this.stack.size();
	}

	@Override
	public boolean isSuspended() {
		return this.suspended;
	}

	@Override
	public void setSuspended(boolean suspended) {
		this.suspended = suspended;
		if (!suspended) {
			this.flush();
		}
	}

	private int sendRequest(List<Request> requests, AsyncCallback<List<CommandResponse>> callback) {
		if (requests == null || requests.isEmpty()) {
			return 0;
		}
		try {
			Collection<Serializer> serializers = Sets.newHashSet();
			List<CommandRequest> commands = Lists.newArrayList();

			for (Request request : requests) {
				serializers.add(request.param.getSerializer());
				commands.add(request.command);
				if (!request.param.isQuiet()) {
					this.fireEvent(new CommandRequestEvent(request.requestId, request.command));
				}
			}

			ServiceCallback serviceCallback = new ServiceCallback(requests, callback);
			CommandServiceCompositeSerializer compositeSerializer = new CommandServiceCompositeSerializer(serializers);

			SerializationStreamFactory streamFactory =
				new CommandSerializationStreamFactory(compositeSerializer, this.moduleBaseURL);
			SerializationStreamWriter streamWriter = streamFactory.createStreamWriter();

			streamWriter.writeString(DefaultCommandController.REMOTE_SERVICE_INTERFACE_NAME);
			streamWriter.writeString(DefaultCommandController.METHOD_NAME);
			streamWriter.writeInt(1);
			streamWriter.writeString(List.class.getName());
			streamWriter.writeObject(commands);

			String payload = streamWriter.toString();

			RpcStatsContext statsContext = new RpcStatsContext();

			RequestCallback responseHandler =
				new RequestCallbackAdapter<List<CommandResponse>>(streamFactory, DefaultCommandController.METHOD_NAME,
					statsContext, serviceCallback, null, ResponseReader.OBJECT);

			this.rpcRequestBuilder.create(this.remoteServiceURL);
			this.rpcRequestBuilder.setCallback(responseHandler);
			this.rpcRequestBuilder.setContentType(DefaultCommandController.RPC_CONTENT_TYPE);
			this.rpcRequestBuilder.setRequestData(payload);
			this.rpcRequestBuilder.setRequestId(statsContext.getRequestId());

			RequestBuilder rb = this.rpcRequestBuilder.finish();
			CsrfController.get().securize(rb);
			rb.send();

			return requests.size();
		} catch (SerializationException e) {
			throw new CommandException(e.getMessage());
		} catch (RequestException e) {
			throw new CommandException(e.getMessage());
		}
	}

	@Override
	public void fireEvent(GwtEvent<?> event) {
		EventBus.get().fireEventFromSource(event, this);
	}

	@Override
	public HandlerRegistration addCommandRequestHandler(CommandRequestEvent.Handler handler) {
		return EventBus.get().addHandlerToSource(CommandRequestEvent.TYPE, this, handler);
	}

	@Override
	public HandlerRegistration addCommandResponseHandler(CommandResponseEvent.Handler handler) {
		return EventBus.get().addHandlerToSource(CommandResponseEvent.TYPE, this, handler);
	}

	public static DefaultCommandController get() {
		if (instance == null) {
			instance = new DefaultCommandController();
		}
		return instance;
	}

}