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;

public class Breakage {

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

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

	}

	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(GeneratedEdit generatedEdit) throws OperationException {
			BufferedDocOp edit = generatedEdit.edit;

			if (generatedEdit.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(
						generatedEdit.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);
						document = Composer.compose(document, transformed.serverOp());
						edit = transformed.clientOp();

						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 GeneratedEdit generateInitialEdit() throws OperationException {
			// our edit will be "Hello" at the end of the document
			int size = documentSize();
			DocOpBuilder docOpBuilder = new DocOpBuilder();
			if (size > 0) {
				docOpBuilder.retain(size);
			}
			BufferedDocOp initialEdit = docOpBuilder.characters("Hello").build();

			// apply initial edit to our internal document
			document = Composer.compose(document, initialEdit);
			edit = initialEdit;

			return new GeneratedEdit(initialEdit, serverRevision);
		}

		public GeneratedEdit generateChatter() throws OperationException {
			int size = documentSize();

			DocOpBuilder docOpBuilder = new DocOpBuilder();
			if (size > 0) {
				docOpBuilder.retain(size);
			}
			BufferedDocOp chatterEdit = docOpBuilder.characters(" Chatter").build();
			// apply chatter edit to our internal document
			document = Composer.compose(document, chatterEdit);
			edit = chatterEdit;

			return new GeneratedEdit(chatterEdit, serverRevision);
		}

		public void generateCachedEdit() throws OperationException {
			int size = documentSize();

			DocOpBuilder docOpBuilder = new DocOpBuilder();
			if (size > 0) {
				docOpBuilder.retain(size);
			}
			BufferedDocOp handbasketEdit = docOpBuilder.characters(" Going to hell").build();

			cachedEdit = handbasketEdit;
		}

		public GeneratedEdit getCachedEdit() throws OperationException {
			// Promote cachedEdit to edit in flight
			edit = cachedEdit;
			document = Composer.compose(document, 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 {

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

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

		System.out.println("\nThree clients, sync'd");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		client3.dumpState();

		/*****************/

		server.acceptEdit(client3.generateChatter());
		client3.acceptUpdates(server.pollForUpdates(client3.getServerRevision()));

		System.out.println("\nClient3 is generating noise");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		client3.dumpState();

		/*****************/

		GeneratedEdit client1GeneratedEditO1 = client1.generateInitialEdit();

		System.out.println("\nClient2 has generated an edit, and it's in flight");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		client3.dumpState();

		/*****************/

		GeneratedEdit client2GeneratedEditOA = client2.generateInitialEdit();

		System.out.println("\nClient2 has generated matching edit, also in flight");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		client3.dumpState();

		/*****************/

		client1.generateCachedEdit();

		System.out.println("\nClient1 has handbasket edit cached");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		client3.dumpState();

		/*****************/

		server.acceptEdit(client2GeneratedEditOA);

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

		System.out.println("\nServer has client2's edit in, and clients synced there");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		client3.dumpState();

		/*****************/

		server.acceptEdit(client3.generateChatter());
		client3.acceptUpdates(server.pollForUpdates(client3.getServerRevision()));

		System.out.println("\nClient3 is generating noise");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		client3.dumpState();

		/*****************/

		server.acceptEdit(client1GeneratedEditO1);

		System.out.println("\nServer accepts client1's edit");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		client3.dumpState();

		/*****************/

		server.acceptEdit(client3.generateChatter());
		client3.acceptUpdates(server.pollForUpdates(client3.getServerRevision()));

		System.out.println("\nClient3 is generating noise");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		client3.dumpState();

		/*****************/

		GeneratedEdit cachedEdit = client1.getCachedEdit();
		server.acceptEdit(cachedEdit);

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

		System.out.println("\nClient1's cached edit posted to server, and accepted");
		server.dumpState();
		client1.dumpState();
		client2.dumpState();
		client3.dumpState();
	}

}
