package com.mouse.hack3;

import java.util.ArrayList;
import java.util.List;

import org.waveprotocol.wave.model.document.operation.AnnotationBoundaryMap;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.AttributesUpdate;
import org.waveprotocol.wave.model.document.operation.BufferedDocOp;
import org.waveprotocol.wave.model.document.operation.DocOpCursor;
import org.waveprotocol.wave.model.document.operation.algorithm.Composer;
import org.waveprotocol.wave.model.document.operation.algorithm.Transformer;
import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder;
import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil;
import org.waveprotocol.wave.model.operation.OpComparators;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.OperationPair;

import com.mouse.hack3.Breakage.Client.GeneratedEdit;

public class Breakage {

	public static class Server {
		// Server state is a kept as the linear history of edits
		private List<BufferedDocOp> editHistory = new ArrayList<BufferedDocOp>();

		public Server() {
			editHistory.add(new DocOpBuilder().build());
		}

		public void acceptEdit(int afterRevision, BufferedDocOp edit) throws OperationException {
			if (afterRevision == editHistory.size()) {
				// Applying at head

				// Confirm the edit composes, and the result is a document
				DocOpUtil.asInitialization(Composer.compose(Composer.compose(editHistory), edit));

				editHistory.add(edit);
			} else {
				// Applying in the past

				// transform edit to bring it into the fold
				OperationPair<BufferedDocOp> transformed = Transformer.transform(edit, Composer.compose(editHistory.subList(afterRevision,
						editHistory.size())));
				edit = transformed.clientOp();

				// Confirm the edit composes, and the result is a document
				DocOpUtil.asInitialization(Composer.compose(Composer.compose(editHistory), edit));

				editHistory.add(edit);
			}
		}

		public List<BufferedDocOp> pollForUpdates(int fromRevision) {

			List<BufferedDocOp> releventHistory;

			if (fromRevision <= editHistory.size()) {
				releventHistory = editHistory.subList(fromRevision, editHistory.size());
			} else {
				releventHistory = new ArrayList<BufferedDocOp>();
			}

			return releventHistory;
		}

		public int getServerRevision() {
			return editHistory.size();
		}

		public void dumpState() {
			int i = 0;
			for (BufferedDocOp edit : editHistory) {
				System.out.println("server editHistory[" + i++ + "]: " + DocOpUtil.toConciseString(edit));
			}
		}
	}

	public static class Client {
		private BufferedDocOp document = new DocOpBuilder().build();
		private int serverRevision = 0;
		private BufferedDocOp edit;
		private BufferedDocOp cachedEdit; // Edits queued up to send later
		private final String clientName;

		public Client(String clientName) {
			this.clientName = clientName;
		}

		public int getServerRevision() {
			return serverRevision;
		}

		public void acceptUpdates(List<BufferedDocOp> updates) throws OperationException {
			for (BufferedDocOp update : updates) {
				serverRevision++;
				if (edit != null) {
					if (OpComparators.SYNTACTIC_IDENTITY.equal(edit, update)) {
						edit = null;
					} else {
						OperationPair<BufferedDocOp> transformed = Transformer.transform(edit, update);
						edit = null;
						document = Composer.compose(document, transformed.serverOp());

						if (null != cachedEdit) {
							// Also transform the queued cachedEdit so that it
							// stays in sync
							cachedEdit = Transformer.transform(cachedEdit, transformed.serverOp()).clientOp();
						}
					}
				} else {
					document = Composer.compose(document, update);
					if (null != cachedEdit) {
						// Also transform the queued cachedEdit so that it
						// stays in sync
						cachedEdit = Transformer.transform(cachedEdit, update).clientOp();
					}
				}
			}
		}

		public void dumpState() {
			System.out.println("Client '" + clientName + "' at server revision " + serverRevision + " with document "
					+ DocOpUtil.toConciseString(document) + (null != edit ? " with edit in flight " + DocOpUtil.toConciseString(edit) : "")
					+ (null != cachedEdit ? " with cached edit " + DocOpUtil.toConciseString(cachedEdit) : ""));
		}

		public class GeneratedEdit {
			public final BufferedDocOp edit;
			public final int afterRevision;

			public GeneratedEdit(BufferedDocOp edit, int afterRevision) {
				super();
				this.edit = edit;
				this.afterRevision = afterRevision;
			}

		}

		public GeneratedEdit generateInitialEdit() throws OperationException {
			// our edit will be "Hello" at the end of the document
			int size = documentSize();
			BufferedDocOp initialEdit;
			if (size > 0) {
				initialEdit = new DocOpBuilder().retain(size).characters("Hello").build();
			} else {
				initialEdit = new DocOpBuilder().characters("Hello").build();
			}
			// apply initial edit to our internal document
			document = Composer.compose(document,initialEdit);
			edit = initialEdit;
			
			return new GeneratedEdit(initialEdit, serverRevision);
		}

		public void generateCachedEdit() {
			int size = documentSize();
			BufferedDocOp handbasketEdit;
			if (size > 0) {
				handbasketEdit = new DocOpBuilder().retain(size).characters("Going to hell").build();
			} else {
				handbasketEdit = new DocOpBuilder().characters("Going to hell").build();
			}
			cachedEdit = handbasketEdit;
		}

		public GeneratedEdit getCachedEdit() {
			BufferedDocOp edit = cachedEdit;
			cachedEdit = null;
			return new GeneratedEdit(edit, serverRevision);
		}

		private int documentSize() {
			final int size[] = { 0 };
			document.apply(new DocOpCursor() {

				@Override
				public void elementStart(String type, Attributes attrs) {
					size[0]++;
				}

				@Override
				public void elementEnd() {
					size[0]++;
				}

				@Override
				public void characters(String chars) {
					size[0] += chars.length();
				}

				@Override
				public void annotationBoundary(AnnotationBoundaryMap map) {
					size[0]++;
				}

				@Override
				public void updateAttributes(AttributesUpdate attrUpdate) {
					size[0]++;
				}

				@Override
				public void retain(int itemCount) {
					size[0] += itemCount;
				}

				@Override
				public void replaceAttributes(Attributes oldAttrs, Attributes newAttrs) {
					size[0]++;
				}

				@Override
				public void deleteElementStart(String type, Attributes attrs) {
					size[0]++;
				}

				@Override
				public void deleteElementEnd() {
					size[0]++;
				}

				@Override
				public void deleteCharacters(String chars) {
					size[0] += chars.length();
				}
			});
			return size[0];
		}

	}

	public static void main(String[] args) throws OperationException {
		/*
		 * 1) There are two clients that both have the the same, empty wave
		 * open.
		 */

		Server server = new Server();
		Client client1 = new Client("Client1");
		Client client2 = new Client("Client2");

		client1.acceptUpdates(server.pollForUpdates(client1.getServerRevision()));
		client2.acceptUpdates(server.pollForUpdates(client2.getServerRevision()));

		System.out.println("State of system after step 1");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();

		/*
		 * 2) Client1 generates O1 and sends it to the server.
		 */

		GeneratedEdit client1GeneratedEditO1 = client1.generateInitialEdit();

		System.out.println("State of system after step 2");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();

		/*
		 * 3) Client2 generates OA and sends it to the server. O1 and OA happen
		 * to be identical.
		 */
		GeneratedEdit client2GeneratedEditOA = client2.generateInitialEdit();

		System.out.println("State of system after step 3");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		/*
		 * 4) Client1 generates O2 and caches it, waiting for the server to
		 * acknowledge O1 before sending O2.
		 */

		client1.generateCachedEdit();
		
		System.out.println("State of system after step 4");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();

		/*
		 * 5) The server decides to apply the two concurrent operations in the
		 * order OA then O1. So, it applies OA after transforming it (the
		 * transformation happens to be a no-op at this stage) and broadcasts OA
		 * to all clients.
		 */
		server.acceptEdit(client2GeneratedEditOA.afterRevision, client2GeneratedEditOA.edit);

		client1.acceptUpdates(server.pollForUpdates(client1.getServerRevision()));
		client2.acceptUpdates(server.pollForUpdates(client2.getServerRevision()));

		server.acceptEdit(client1GeneratedEditO1.afterRevision, client1GeneratedEditO1.edit);

		System.out.println("State of system after step 5");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();

		/*
		 * 6) Client1 receives OA, compares it to its unacknowledged operation,
		 * O1. They are the same, so Client1 incorrectly assumes that the server
		 * has acknowledged O1.
		 */

		// See above dump state. In short, yup.

		/*
		 * 7) Client1 sends OA to the server and we all go to hell in a hand
		 * basket as the server is not expecting Client1 to send operations at
		 * this time.
		 */

		GeneratedEdit cachedEdit = client1.getCachedEdit();
		server.acceptEdit(cachedEdit.afterRevision, cachedEdit.edit);

		client1.acceptUpdates(server.pollForUpdates(client1.getServerRevision()));
		client2.acceptUpdates(server.pollForUpdates(client2.getServerRevision()));

		System.out.println("State of system after step 7");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
	}

}
