/******************************************************************************* 
 * @license
 * Copyright (c) 2011, 2012 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials are made 
 * available under the terms of the Eclipse Public License v1.0 
 * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
 * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
 * 
 * Contributors: IBM Corporation - initial API and implementation 
 ******************************************************************************/
/*jslint browser:true regexp:true*/
/*global console define*/
define("orion/editor/AsyncStyler", ['i18n!orion/editor/nls/messages', 'orion/editor/annotations'], function(messages, mAnnotations) {
	var SERVICE_NAME = "orion.edit.highlighter";
	var HIGHLIGHT_ERROR_ANNOTATION = "orion.annotation.highlightError";
	var badServiceError = SERVICE_NAME + " service must be an event emitter";
	mAnnotations.AnnotationType.registerType(HIGHLIGHT_ERROR_ANNOTATION, {
		title: messages.syntaxError,
		html: "<div class='annotationHTML error'></div>",
		rangeStyle: {styleClass: "annotationRange error"}
	});

	function isRelevant(serviceReference) {
		return serviceReference.getProperty("objectClass").indexOf(SERVICE_NAME) !== -1 &&
				serviceReference.getProperty("type") === "highlighter";
	}

	/**
	 * @name orion.editor.AsyncStyler
	 * @class Provides asynchronous styling for a TextView using registered "highlighter" services.
	 * @description Creates an <code>AsyncStyler</code>. An AsyncStyler allows style information to be sent to a 
	 * <code>TextView</code> asynchronously through the service segistry. Style is generated by <em>style providers</em>, which are services
	 * having the <code>'orion.edit.highlighter'</code> service name and a <code>type</code> === <code>'highlighter'</code> service property.
	 *
	 * <p>A style provider monitors changes to the TextView (typically using an <code>orion.edit.model</code> service) and 
	 * dispatches a service event of type <code>'orion.edit.highlighter.styleReady'</code> when it has style information to send.
	 * The event carries a payload of style information for one or more lines in the TextView. The AsyncStyler then applies
	 * the style information fron the event to the TextView using the {@link orion.editor.TextView#event:onLineStyle} API.
	 * </p>
	 *
	 * <p>Applying style information may cause the TextView to be redrawn, which is potentially expensive. To minimize the 
	 * number of redraws, a provider should provide style for many lines in a single StyleReadyEvent.
	 * </p>
	 *
	 * @param {orion.editor.TextView} textView The TextView to style.
	 * @param {orion.serviceregistry.ServiceRegistry} serviceRegistry The ServiceRegistry to monitor for highlighter services.
	 * @param {orion.editor.AnnotationModel} [annotationModel] The Annotation Model to use for creating error and warning annotations.
	 * @see orion.editor.StyleReadyEvent
	 */
	function AsyncStyler(textView, serviceRegistry, annotationModel) {
		this.initialize(textView, serviceRegistry, annotationModel);
		this.lineStyles = [];
	}
	AsyncStyler.prototype = /** @lends orion.editor.AsyncStyler.prototype*/ {
		/** @private */
		initialize: function(textView, serviceRegistry, annotationModel) {
			this.textView = textView;
			this.serviceRegistry = serviceRegistry;
			this.annotationModel = annotationModel; 
			this.services = [];

			var self = this;
			this.listener = {
				onModelChanging: function(e) {
					self.onModelChanging(e);
				},
				onModelChanged: function(e) {
					self.onModelChanged(e);
				},
				onDestroy: function(e) {
					self.onDestroy(e);
				},
				onLineStyle: function(e) {
					self.onLineStyle(e);
				},
				onStyleReady: function(e) {
					self.onStyleReady(e);
				},
				onServiceAdded: function(serviceEvent) {
					self.onServiceAdded(serviceEvent.serviceReference, self.serviceRegistry.getService(serviceEvent.serviceReference));
				},
				onServiceRemoved: function(serviceEvent) {
					self.onServiceRemoved(serviceEvent.serviceReference, self.serviceRegistry.getService(serviceEvent.serviceReference));
				}
			};
			textView.addEventListener("ModelChanging", this.listener.onModelChanging);
			textView.addEventListener("ModelChanged", this.listener.onModelChanged);
			textView.addEventListener("Destroy", this.listener.onDestroy);
			textView.addEventListener("LineStyle", this.listener.onLineStyle);
			serviceRegistry.addEventListener("registered", this.listener.onServiceAdded);
			serviceRegistry.addEventListener("unregistering", this.listener.onServiceRemoved);

			var serviceRefs = serviceRegistry.getServiceReferences(SERVICE_NAME);
			for (var i = 0; i < serviceRefs.length; i++) {
				var serviceRef = serviceRefs[i];
				if (isRelevant(serviceRef)) {
					this.addServiceListener(serviceRegistry.getService(serviceRef));
				}
			}
		},
		/** @private */
		onDestroy: function(e) {
			this.destroy();
		},
		/** Deactivates this styler and removes any listeners it registered. */
		destroy: function() {
			if (this.textView) {
				this.textView.removeEventListener("ModelChanging", this.listener.onModelChanging);
				this.textView.removeEventListener("ModelChanged", this.listener.onModelChanged);
				this.textView.removeEventListener("Destroy", this.listener.onDestroy);
				this.textView.removeEventListener("LineStyle", this.listener.onLineStyle);
				this.textView = null;
			}
			if (this.services) {
				for (var i = 0; i < this.services.length; i++) {
					this.removeServiceListener(this.services[i]);
				}
				this.services = null;
			}
			if (this.serviceRegistry) {
				this.serviceRegistry.removeEventListener("registered", this.listener.onServiceAdded);
				this.serviceRegistry.removeEventListener("unregistering", this.listener.onServiceRemoved);
				this.serviceRegistry = null;
			}
			this.listener = null;
			this.lineStyles = null;
		},
		/** @private */
		onModelChanging: function(e) {
			this.startLine = this.textView.getModel().getLineAtOffset(e.start);
		},
		/** @private */
		onModelChanged: function(e) {
			var startLine = this.startLine;
			if (e.addedLineCount || e.removedLineCount) {
				Array.prototype.splice.apply(this.lineStyles, [startLine, e.removedLineCount].concat(this._getEmptyStyle(e.addedLineCount)));
			}
		},
		/** @private */
		onStyleReady: function(e) {
			var style = e.lineStyles || e.style;
			var min = Number.MAX_VALUE, max = -1;
			var model = this.textView.getModel();
			for (var lineIndex in style) {
				if (Object.prototype.hasOwnProperty.call(style, lineIndex)) {
					this.lineStyles[lineIndex] = style[lineIndex];
					min = Math.min(min, lineIndex);
					max = Math.max(max, lineIndex);
				}
			}
//			console.debug("Got style for lines " + (min+1) + " to " + (max+1));
			min = Math.max(min, 0);
			max = Math.min(max, model.getLineCount());
			
			var annotationModel = this.annotationModel;
			if (annotationModel) {
				var annos = annotationModel.getAnnotations(model.getLineStart(min), model.getLineEnd(max));
				var toRemove = [];
				while (annos.hasNext()) {
					var anno = annos.next();
					if (anno.type === HIGHLIGHT_ERROR_ANNOTATION) {
						toRemove.push(anno);
					}
				}
				var toAdd = [];
				for (var i = min; i <= max; i++) {
					lineIndex = i;
					var lineStyle = this.lineStyles[lineIndex], errors = lineStyle && lineStyle.errors;
					var lineStart = model.getLineStart(lineIndex);
					if (errors) {
						for (var j=0; j < errors.length; j++) {
							var err = errors[j];
							toAdd.push(mAnnotations.AnnotationType.createAnnotation(HIGHLIGHT_ERROR_ANNOTATION, err.start + lineStart, err.end + lineStart));
						}
					}
				}
				annotationModel.replaceAnnotations(toRemove, toAdd);
			}
			this.textView.redrawLines(min, max + 1);
		},
		/** @private */
		onLineStyle: function(e) {
			function _toDocumentOffset(ranges, lineStart) {
				var len = ranges.length, result = [];
				for (var i=0; i < len; i++) {
					var r = ranges[i];
					result.push({
						start: r.start + lineStart,
						end: r.end + lineStart,
						style: r.style
					});
				}
				return result;
			}
			var style = this.lineStyles[e.lineIndex];
			if (style) {
				// The 'ranges', 'errors' are of type {@link orion.editor.LineStyleEvent#ranges}, except the 
				// start and end indices are line-relative offsets, not document-relative.
				if (style.ranges) { e.ranges = _toDocumentOffset(style.ranges, e.lineStart); }
				else if (style.style) { e.style = style.style; }
			}
		},
		/** @private */
		_getEmptyStyle: function(n) {
			var result = [];
			for (var i=0; i < n; i++) {
				result.push(null);
			}
			return result;
		},
		/**
		 * Sets the content type ID for which style information will be provided. The ID will be passed to all style provider 
		 * services monitored by this AsyncStyler by calling the provider's own <code>setContentType(contentTypeId)</code> method.
		 *
		 * <p>In this way, a single provider service can be registered for several content types, and select different logic for 
		 * each type.</p>
		 * @param {String} contentTypeId The Content Type ID describing the kind of file currently being edited in the TextView.
		 * @see orion.core.ContentType
		 */
		setContentType: function(contentTypeId) {
			this.contentType = contentTypeId;
			if (this.services) {
				for (var i = 0; i < this.services.length; i++) {
					var service = this.services[i];
					if (service.setContentType) {
						var progress = this.serviceRegistry.getService("orion.page.progress");
						if(progress){
							progress.progress(service.setContentType(this.contentType), "Styling content type: " + this.contentType.id ? this.contentType.id: this.contentType);
						} else {
							service.setContentType(this.contentType);
						}
					}
				}
			}
		},
		/** @private */
		onServiceAdded: function(serviceRef, service) {
			if (isRelevant(serviceRef)) {
				this.addServiceListener(service);
			}
		},
		/** @private */
		onServiceRemoved: function(serviceRef, service) {
			if (this.services.indexOf(service) !== -1) {
				this.removeServiceListener(service);
			}
		},
		/** @private */
		addServiceListener: function(service) {
			if (typeof service.addEventListener === "function") {
				service.addEventListener("orion.edit.highlighter.styleReady", this.listener.onStyleReady);
				this.services.push(service);
				if (service.setContentType && this.contentType) {
					var progress = this.serviceRegistry.getService("orion.page.progress");
					if(progress){
						progress.progress(service.setContentType(this.contentType), "Styling content type: " + this.contentType.id ? this.contentType.id: this.contentType);
					} else {
						service.setContentType(this.contentType);
					}
				}
			} else {
				if (typeof console !== "undefined") {
					console.log(new Error(badServiceError));
				}
			}
		},
		/** @private */
		removeServiceListener: function(service) {
			if (typeof service.removeEventListener === "function") {
				service.removeEventListener("orion.edit.highlighter.styleReady", this.listener.onStyleReady);
				var serviceIndex = this.services.indexOf(service);
				if (serviceIndex !== -1) {
					this.services.splice(serviceIndex, 1);
				}
			} else {
				if (typeof console !== "undefined") {
					console.log(new Error(badServiceError));
				}
			}
		}
	};

	/**
	 * @name orion.editor.StyleReadyEvent
	 * @class Represents the styling for a range of lines, as provided by a service.
	 * @description Represents the styling for a range of lines, as provided by a service.
	 * @property {Object} lineStyles A map of style information. Each key of the map is a line index, and the value 
	 * is a {@link orion.editor.StyleReadyEvent#LineStyle} giving the style information for the line.
	 */
	/**
	 * @name orion.editor.StyleReadyEvent#LineStyle
	 * @class Represents style information for a line.
	 * @description Represents style information for a line.
	 * <p>Note that the offsets given in the {@link #ranges} and {@link #errors} properties are relative to the start of the
	 * line that this LineStyle is associated with, not the start of the document.</p>
	 * @property {orion.editor.StyleRange[]} ranges Optional; Gives the styles for this line.
	 * @property {orion.editor.StyleRange[]} errors Optional; Gives the error styles for this line. Error styles will be 
	 * presented as annotations in the UI.
	 */

	return AsyncStyler;
});