/**
 * @name widget_webview.js
 * @fileOverview TBD documentation;
 * See {@link Mojo.Widget.WebView} for more info.
 */
if (Mojo.WebView === undefined) {
	Mojo.WebView = {};
}

Mojo.WebView.Rectangle = Class.create({

	initialize: function(left, top, right, bottom) {
		this.left = left;
		this.top = top;
		this.right = right;
		this.bottom = bottom;
	},

	equals: function(rect) {
		return this.left === rect.left && this.top === rect.top &&
					this.right === rect.right && this.bottom === rect.bottom;
	},

	toString: function() {
		return "L: " + this.left + ", T: " + this.top + ", R: " + this.right + ", B: " + this.bottom;
	}
});

/*
 * An entry in the browser forward/back (i.e. session) history.
 * @param {String} url The page's URL.
 * @param {Number} scrollX	The page X scroll position.
 * @param {Number} scrollY	The page Y scroll position.
 * @param {Number} zoom	 The page zoom level.
 */
Mojo.WebView.HistoryItem = Class.create({

	initialize: function(url, scrollX, scrollY, width, height, zoom) {
		this.url = url;
		this.scrollX = scrollX || 0;
		this.scrollY = scrollY || 0;
		this.width = width || 320;
		this.height = height || 480;
		this.zoom = zoom || 1.0;
	}
});

Mojo.WebView.MetaViewport = Class.create({

	initialize: function(initialScale, minimumScale, maximumScale, width, height, userScalable) {
		this.initialScale = initialScale;
		this.minimumScale = minimumScale;
		this.maximumScale = maximumScale;
		this.width = width;
		this.height = height;
		this.userScalable = userScalable;
	}
});

/*
 * @private
 * @constant
 * @description The first paint (rendering) of the web page has finished. More may
 *              follow as the page continues to load.
 *
 * Supported Widgets
 *
 * - {@link Mojo.Widget.WebView}
 */
Mojo.Event.webViewFirstPaintComplete = 'mojo-webview-first-paint-complete';

/**
#### Overview ####
To embed a contained web object, you would declare and instantiate a webView widget. You can bound the
widget within your declaration to get a scrolling view, or allow it to take over an entire scene at
your discretion. You can use it render local markup or to load an external URL; as long as you can
define the source as a URL it can be applied.


#### Declaration ####

		<div x-mojo-element="WebView" id="WebId" class="WebClass" name="WebName"></div>

		Properties		Required	Value			Description
		---------------------------------------------------------------------------------------------------------------------------------
	    x-mojo-element	Required	WebView			Declares the widget as type 'WebView'
	    id				Required	Any String		Identifies the widget element for use when instantiating or rendering
	    class			Optional	Any String		Provide your own unique class and override the frameworks styles
	    name			Optional	Any String		Add a unique name to the WebView widget; generally used in templates when used


#### Events ####

		Mojo.Event.listen("WebId",'mojo-webViewLoadProgress', this.handleUpdate)

		Event Type					Value			Event Handling
		---------------------------------------------------------------------------------------------------------------------------------
		Mojo.Event.webViewLoadProgress
		Mojo.Event.webViewLoadStarted
		Mojo.Event.webViewLoadStopped
		Mojo.Event.webViewLoadFailed
		Mojo.Event.webViewDownloadFinished
		Mojo.Event.webViewLinkClicked
		Mojo.Event.webViewDownloadFinished
		Mojo.Event.webViewTitleUrlChanged
		Mojo.Event.webViewTitleChanged
		Mojo.Event.webViewUrlChanged
		Mojo.Event.webViewCreatePage
		Mojo.Event.webViewTapRejected
		Mojo.Event.webViewScrollAndScaleChanged
		Mojo.Event.webViewEditorFocused
		Mojo.Event.webViewUpdateHistory
		Mojo.Event.webViewSetMainDocumentError
		Mojo.Event.webViewServerConnect
		Mojo.Event.webViewServerDisconnect
		Mojo.Event.webViewResourceHandoff
		Mojo.Event.webViewFirstPaintComplete
		Mojo.Event.webViewUrlRedirect
		Mojo.Event.webViewModifierTap
		Mojo.Event.webViewMimeNotSupported
		Mojo.Event.webViewMimeHandoff


#### Instantiation ####

		this.controller.setupWidget("WebId",
			this.attributes = {
				url:	'http://www.palm.com',
				minFontSize:18,
				virtualpagewidth: 20,
				virtualpageheight: 10
				},
			this.model = {
				}
		});


#### Attribute Properties ####

		Attribute Property	Type		Required	Default		Description
		---------------------------------------------------------------------------------------------------------------------------------
	    virtualpageheight   Integer     Optional    ?           The browser's virtual page height
	    virtualpagewidth    Integer     Optional    ?           The browser's virtual page width
	    urL                 String      Required    None        The initial URL to display
	    pageIdentifier      String      Optional    ?           The BrowserServer page identifier. This is used when the BrowserServer
																instructs an application to open a new URL
	    minFontSize         Integer     Optional    ?           The minimum font size that the browser will display
	    topMargin           Integer     Optional    ?           The margin above the web view that is scrolled off the screen when a new page is loaded
	    cacheAdapter        Boolean     Optional    undefined   If true, cache this adapter, false if not, or undefined to not specify and use the
																browser-adapter default. Default is undefined.
	    interrogateClicks   Boolean     Optional    true        If the host application wants to be called for every hyperlink click via
																Mojo.Event.webViewLinkClicked event
	    showClickedLink		Boolean     Optional    true        Styles clicked link with grey background and border.


#### Model Properties ####

		Model Property		Type			Required	Default		Description
		---------------------------------------------------------------------------------------------------------------------------------
		None


#### Methods ####

		Method				Arguments			Description
		---------------------------------------------------------------------------------------------------------------------------------
		None

#### Styles ####

		Class Names			Properties
		---------------------------------------------------------------------------------------------------------------------------------
		None


@field
*/
Mojo.Widget.WebView = Class.create({

	smartZoomScaleSlopFactor: 1.05,
    mouseDownEventType: 0,
    mouseUpEventType: 1,
    mouseMoveEventType: 2,
	maxScaleFactor: 2.0,
	adapterNum: 0,

	initialize: function() {

		this.useAdapter = Mojo.Host.current === Mojo.Host.palmSysMgr;
	    this.adapter = 0;
	    this.sceneScroller = null;
	    this.tapIndex = 0;
		this.currScale = 1.0;
        this.currPageWidth = 0;
        this.currPageHeight = 0;
		this.fitWidth = true;
		this.metaViewport = undefined;
		this.adapterConnected = true;
		this.zoomAtGestureStart = this.currScale;
        this.offsetAtGestureStart = {left: 0, top: 0};
		this.javascriptDialog = null;
		this.useMouseEvents = false;
		this.historyInfo = {};
        this._lastTapEvent = undefined;
		this._deleteLastTapEvent = this._deleteLastTapEvent.bind(this);
		this.lastUrl = null;

        this.inPinchZoom = false;
        this.pinchZoomAnimationTimer = null;
        this.pinchZoomCurrentScale = 1.0;
        this.pinchZoomTargetScale = 1.0;
        this.pinchZoomCurrentCenter = { left: 0, top: 0 };
        this.pinchZoomTargetCenter = { left: 0, top: 0 };
		this.prevWindowSize = {width: 0, height: 0};
        this.prevScrollPosition = {x: 0, y: 0};

		this.hasFocus = true;
		this.topMargin = 0;
		this._doubleClickHandler = this._handleDoubleClick.bind(this);
		this._singleTapHandler = this._handleSingleTap.bind(this);
		this._tapHandler = this._handleTap.bind(this);
		this._dragStartHandler = this._handleDragStart.bindAsEventListener(this);
		this._gestureStartHandler = this._handleGestureStart.bind(this);
		this._gestureChangeHandler = this._handleGestureChange.bind(this);
		this._gestureEndHandler = this._handleGestureEnd.bind(this);
		this._windowResizeHandler = this._handleWindowResize.bindAsEventListener(this);
		this._addAsScrollListener = this._addAsScrollListener.bind(this);
		this._cardActivate = this._cardActivate.bindAsEventListener(this);
		this._cardDeactivate = this._cardDeactivate.bindAsEventListener(this);
		this._activateHandler = this._activate.bind(this);
		this._deactivateHandler = this._deactivate.bind(this);
		this._connectAdapterToServer = this._connectAdapterToServer.bind(this);
        this._mouseDownHandler = this._handleMouseDown.bindAsEventListener(this);
        this._mouseUpHandler = this._handleMouseUp.bindAsEventListener(this);
        this._mouseMoveHandler = this._handleMouseMove.bindAsEventListener(this);

		this.popupCallbacks = [];
	},

	/**
	 * A helper function to sanitize incoming strings from the
	 * browserserver (removes tags and scripts)
	 */
	$X: function(val) {
		return val.stripScripts().stripTags();
	},

    setup: function() {

		try {
			if (this.controller.attributes.minimumpageheight === undefined) {
				this.controller.attributes.minimumpageheight = this.controller.window.innerHeight;
			}

			if (this.useAdapter) {
				// Create the browser adapter
				this.adapter = this.controller.document.createElement("object");

				this.adapter.setAttribute('x-palm-pass-event', true);
				this.adapter.setAttribute(Mojo.Gesture.consumesEnterAttribute, true);
				this.adapter.setAttribute("type", "application/x-palm-browser");
				if (this.controller.attributes.cacheAdapter !== undefined) {
					this.adapter.setAttribute("x-palm-cache-plugin", this.controller.attributes.cacheAdapter ? 'true' : 'false');
				}
				if (this.controller.attributes.virtualpagewidth) {
					this.adapter.setAttribute("virtualPageWidth", this.controller.attributes.virtualpagewidth);
				}
				if (this.controller.attributes.virtualpageheight) {
					this.adapter.setAttribute("virtualPageHeight", this.controller.attributes.virtualpageheight);
				} else {
					this.controller.attributes.virtualpageheight = this.controller.window.innerHeight;
				}
				if (this.controller.attributes.useMouseEvents) {
					this.adapter.setAttribute("useMouseEvents", this.controller.attributes.useMouseEvents);
					this.useMouseEvents = true;
				}
				if (this.controller.attributes.fitWidth === undefined) {
					this.controller.attributes.fitWidth = true;
				}
				this.fitWidth = this.controller.attributes.fitWidth;

				this.adapter.eventListener = this;
			}
			else {
				// Create a test image we can use in place of the adapter.
				this.adapter = this.controller.document.createElement("img");
				this.adapter.setAttribute("src", "icon.png");
				this.adapter.setStyle("width: 100%; height: 100%; -webkit-user-drag: none;");
			}

			this.adapter.id = "browser-adapter-" + this.adapterNum;
			this.adapterNum += 1;

			if (this.controller.attributes.topMargin) {
				this.topMargin = this.controller.attributes.topMargin;
			}

			this.prevWindowSize.width = this.controller.window.innerWidth;
			this.prevWindowSize.height = this.controller.window.innerHeight;

			// Initialize the width, height to that of the window. This is used for
			// setting the initial dimensions of the adapter view.
			// FIXME: This is used only  should be configurable by the caller
			this.adapter.setAttribute("width", this.controller.window.innerWidth);
			this.adapter.setAttribute("height", this.controller.attributes.virtualpageheight);

			this._setDimensions(this.adapter,
								this.controller.window.innerWidth,
								this.controller.window.innerHeight);

            this.topScroller = Mojo.View.getScrollerForElement(this.controller.element);
			this.sceneScroller = this.topScroller._mojoController.assistant;

			this.controller.element.update(this.adapter);

			this.controller.document.addEventListener(Mojo.Event.activate, this._cardActivate, false);
			this.controller.document.addEventListener(Mojo.Event.deactivate, this._cardDeactivate, false);
			this.controller.scene.listen(this.controller.scene.sceneElement, Mojo.Event.activate, this._activateHandler);
			this.controller.scene.listen(this.controller.scene.sceneElement, Mojo.Event.deactivate, this._deactivateHandler);

			if (this.useMouseEvents) {
                Mojo.Event.listen(this.adapter, 'mousedown', this._mouseDownHandler, true);
                Mojo.Event.listen(this.adapter, 'mouseup', this._mouseUpHandler, true);
                Mojo.Event.listen(this.adapter, 'mousemove', this._mouseMoveHandler, true);
            }
            else {
	        	Mojo.Event.listen(this.adapter, 'dblclick', this._doubleClickHandler, true);
	        	Mojo.Event.listen(this.adapter, Mojo.Event.tap, this._tapHandler, false);
	        	Mojo.Event.listen(this.controller.document, Mojo.Event.singleTap, this._singleTapHandler, false);
            }

			this.controller.exposeMethods(['setTopMargin',
						"clearCache", "clearCookies", "deleteImage", "generateIconFromFile",
						"goBack",  "goForward",	"openURL",  "reloadPage", "resizeImage",
						"getHistoryState", "setBlockPopups", "setAcceptCookies", "addUrlRedirect", "addSystemRedirects",
						"saveViewToFile", "setEnableJavaScript", "stopLoad", "sendDialogResponse",
						       "registerOnPopup", "unregisterOnPopup", "focus", "blur", "clearHistory", "setShowClickedLink"]);

			Mojo.Log.info("WebView widget is setup");
		}
		catch (e) {
			Mojo.Log.logException(e, 'setup');
		}
    },

    cleanup: function() {

		this.controller.document.removeEventListener(Mojo.Event.activate, this._cardActivate, false);
		this.controller.document.removeEventListener(Mojo.Event.deactivate, this._cardDeactivate, false);
		this.controller.scene.stopListening(this.controller.scene.sceneElement, Mojo.Event.activate, this._activateHandler);
		this.controller.scene.stopListening(this.controller.scene.sceneElement, Mojo.Event.deactivate, this._deactivateHandler);


        if (this.useMouseEvents) {
            Mojo.Event.stopListening(this.adapter, 'mousedown', this._mouseDownHandler, true);
            Mojo.Event.stopListening(this.adapter, 'mouseup', this._mouseUpHandler, true);
            Mojo.Event.stopListening(this.adapter, 'mousemove', this._mouseMoveHandler, true);
        }
        else {
            Mojo.Event.stopListening(this.adapter, 'dblclick', this._doubleClickHandler, true);
            Mojo.Event.stopListening(this.adapter, Mojo.Event.tap, this._tapHandler, false);
			Mojo.Event.stopListening(this.controller.document, Mojo.Event.singleTap, this._singleTapHandler, false);
        }

        this.adapter = null;
    },

	_activate: function() {

		Mojo.Log.info("WebView#_activate()");
        Mojo.Event.listen(this.controller.document, 'gesturestart', this._gestureStartHandler, false);
        Mojo.Event.listen(this.controller.document, 'gesturechange', this._gestureChangeHandler, false);
        Mojo.Event.listen(this.controller.document, 'gestureend', this._gestureEndHandler, false);
        Mojo.Event.listen(this.controller.element, Mojo.Event.dragStart, this._dragStartHandler, false);
		Mojo.Event.listen(this.topScroller, Mojo.Event.scrollStarting, this._addAsScrollListener, false);
		Mojo.Event.listen(this.controller.window, 'resize', this._windowResizeHandler, false);

		// The scene scroller reverts to vertical when it is re-shown so we set it each time
		// the scene is activated.
		this.topScroller.mojo.setMode("dominant");

		if (this.useAdapter) {
			this.adapter.pageFocused(this.hasFocus);
		}
	},

	_deactivate: function() {

		Mojo.Log.info("WebView#_deactivate()");
        Mojo.Event.stopListening(this.controller.document, 'gesturestart', this._gestureStartHandler, false);
        Mojo.Event.stopListening(this.controller.document, 'gesturechange', this._gestureChangeHandler, false);
        Mojo.Event.stopListening(this.controller.document, 'gestureend', this._gestureEndHandler, false);
        Mojo.Event.stopListening(this.controller.element, Mojo.Event.dragStart, this._dragStartHandler, false);
		Mojo.Event.stopListening(this.topScroller, Mojo.Event.scrollStarting, this._addAsScrollListener, false);
		Mojo.Event.stopListening(this.controller.window, 'resize', this._windowResizeHandler, false);
	},

	clearCache: function() {
		if (this.useAdapter) {
			this.adapter.clearCache();
		}
	},

	clearCookies: function() {
		if (this.useAdapter) {
			this.adapter.clearCookies();
		}
	},

	deleteImage: function(image) {
		if (this.useAdapter) {
			this.adapter.deleteImage(image);
		}
	},

	generateIconFromFile: function(src, dst, left, top, right, bottom) {
		if (this.useAdapter) {
			this.adapter.generateIconFromFile(src, dst, left, top, right, bottom);
		}
	},

	getHistoryState: function(onSuccess) {
		if (this.useAdapter) {
			this.adapter.getHistoryState(onSuccess);
		}
		else {
			// Simulate always being able to go forward/back
			onSuccess(true, true);
		}
	},

	goBack: function() {
		this._saveCurrentPagePosition();
		if (this.useAdapter) {
			this.adapter.goBack();
		}
	},

	goForward: function() {
		this._saveCurrentPagePosition();
		if (this.useAdapter) {
			this.adapter.goForward();
		}
	},

	clearHistory: function() {
		if (this.useAdapter) {
			this.adapter.clearHistory();
		}
		this.historyInfo = {};
	},

	openURL: function(url) {
		if (this.useAdapter) {
			this.adapter.openURL(url);
		}
		else {
			this.loadStarted();
			this.pageDimensions(0, 0);
			this.titleURLChange("Adapterless", url, false, false);
			this.pageDimensions(500, 1000);
			this.loadStopped();
		}
	},

	reloadPage: function() {
		if (this.useAdapter) {
			this.adapter.reloadPage();
		}
	},

	resizeImage: function(src, dst, width, height) {
		this.adapter.resizeImage(src, dst, width, height);
	},

	addUrlRedirect: function(url, redirect, userData, type) {
		if (this.useAdapter) {
			this.adapter.addUrlRedirect(url, redirect, userData, type);
		}
	},

	/**
	 * Save the current view to the specified file.
	 *
	 * @param {String} fname  The output file name. (required)
	 * @param {Number} left   Left view relative coordinate. (optional)
	 * @param {Number} top    Top view relative coordinate. (optional)
	 * @param {Number} width  Width of view to save. (optional)
	 * @param {Number} height Height of view to save. (optional)
	 */
	saveViewToFile: function(fname, left, top, width, height) {
		if (this.useAdapter) {
			if (left === undefined && top === undefined && width === undefined && height === undefined) {
				this.adapter.saveViewToFile(fname);
			}
			else {
				this.adapter.saveViewToFile(fname, left, top, width, height);
			}
		}
	},

	setEnableJavaScript: function(value) {
		if (this.useAdapter) {
			this.adapter.setEnableJavaScript(value);
		}
	},

	setBlockPopups: function(value) {
		if (this.useAdapter) {
			this.adapter.setBlockPopups(value);
		}
	},

	setAcceptCookies: function(value) {
		if (this.useAdapter) {
			this.adapter.setAcceptCookies(value);
		}
	},

	stopLoad: function() {
		if (this.useAdapter) {
			this.adapter.stopLoad();
		}
	},

	_cardActivate: function(event) {
		this.hasFocus = true;

		if (this.useAdapter) {
			try {
				this.adapter.pageFocused(this.hasFocus);
			}
			catch (e) {} // Can fail if page scene is not active.
		}

		if (!this.adapterConnected) {
			// This browser page was purged when the application was not in the foreground.
			// Now that we're active again let's reconnect and reload the page.
			Mojo.Log.info("Reconnecting adapter to server.");
			this._connectAdapterToServer();
		}
	},

	_cardDeactivate: function(event) {
		this.hasFocus = false;

		if (this.useAdapter) {
			try {
				this.adapter.pageFocused(this.hasFocus);
			}
			catch (e) {} // Can fail if page scene is not active.
		}

		// Javascript dialog must be dismissed if the window loses focus
		if (this.popupWidgetElement) {
			this.popupWidgetElement.mojo.close();
			this.popupWidgetElement = null;
		}

		// Going in the background stop trying to reconnect to the browser server.
		if (this._serverConnectTimer) {
			this.controller.window.clearTimeout(this._serverConnectTimer);
			delete this._serverConnectTimer;
		}
	},

	/**
	 * Map coordinates from the page coordinates system to the scene coordinate system.
	 *
	 * @param {Object} pagePos Page position (origin TL).
	 */
	_pageToScene: function(pagePos) {
		return {left: -pagePos.left * this.currScale, top: -pagePos.top * this.currScale};
	},

	_sceneToPage: function(scenePos) {
		if (this.currScale !== 0.0) {
			return { left: -scenePos.left / this.currScale, top: -scenePos.top / this.currScale };
		}
		else {
			return {left: 0, top: 0};
		}
	},

	/**
	 * Handle a window resize event. We get this when the screen orientation changes.
	 *
	 * @param {Event} event
	 */
 	_handleWindowResize: function(event) {

		var width = this.controller.window.innerWidth;
		var height= this.controller.window.innerHeight;

		try {
			if (width === this.prevWindowSize.width && height === this.prevWindowSize.height) {
				return;
			}

			if (this.adapter.setViewportSize) { // Adapter could be unloaded
				this.adapter.setViewportSize(width, height);
			}

			// Calculate the current page center before anything is changed.
			var pageTL = this._sceneToPage(this.sceneScroller.getScrollPosition());

			// Resize the adapter
			this.currScale = this.currScale * width / this.prevWindowSize.width;
			this._setDimensions(this.adapter,
						this.currPageWidth * this.currScale,
						this.currPageHeight * this.currScale);

			// Scroll so that the previous center is still in the center of the screen.
			var sceneTL = this._pageToScene(pageTL);

			this.sceneScroller.scrollTo(sceneTL.left, sceneTL.top);

			var coordSysChanged = this.prevWindowSize.width === width;
			this.prevWindowSize.width = width;
			this.prevWindowSize.height = height;

			if (coordSysChanged) {
				this._clearAutoZoomInfo();
			}
		} catch (e) {
			Mojo.Log.logException(e, '_handleWindowResize');
		}
	},

	/**
	 * @private
	 */
	_clearAutoZoomInfo: function() {
		delete this.lastZoomRectangle;
		this.fitWidth = this.controller.attributes.fitWidth;
	},

    _handleDoubleClick: function(event) {
		try {
			delete this._lastTapEvent;

			if (this.metaViewport) {
				if (!this.metaViewport.userScalable) {
					return;
				}
			}

	        var scaledPos = Element.viewportOffset(this.adapter);

	        scaledPos.left = event.clientX - scaledPos.left;
	        scaledPos.top  = event.clientY - scaledPos.top;

			if (this.useAdapter) {
				this.adapter.smartZoom(scaledPos.left, scaledPos.top);
			}
			else {
				this.smartZoomCalculateResponseSimple(10, 10, 1000, 2000, 100, 100);
			}
		}
		catch (e) {
			Mojo.Log.logException(e, '_handleDoubleClick');
		}
    },

	_handleSingleTap: function(event) {

		try {
			this.tapIndex += 1;

			var offset = Element.viewportOffset(this.adapter);

			offset.left = event.x - offset.left;
			offset.top  = event.y - offset.top;

			if (this._lastTapEvent && this.useAdapter && (this.adapter === this._lastTapEvent.up.target)) {
				if (this._lastTapEvent.up.shiftKey || this._lastTapEvent.up.altKey) {

					this.adapter.inspectUrlAtPoint(offset.left, offset.top, function(response) {

						Mojo.Event.send(this.adapter, Mojo.Event.webViewModifierTap,
							{up: this._lastTapEvent, linkInfo: response});
						delete this._lastTapEvent;
					}.bind(this));
				}
				else {
					this.adapter.clickAt(offset.left, offset.top, this.tapIndex);
				}
			}
		}
		catch (e) {
			Mojo.Log.logException(e, 'handleSingleTap');
		}
	},

	_deleteLastTapEvent: function() {
		// A hack until NOV-40833 is implemented whereby we will no
		// longer need to save the last tap event.
		delete this._lastTapEvent;
	 },

	_handleTap: function(event) {

		// Record current tap event for use in the
		// single tap handler to determine if we should propagate
		// the event to the adapter. Delete after NOV-40833 is implemented.
		this._lastTapEvent = event;
		this._deleteLastTapEvent.delay(0.5);
	},

	moved: function(scrollEnding, position) {
            if (this.adapter) {

                // current curve for scrolling is too shallow towards end.
                // we want to inform the adapter much before that
                if (scrollEnding ||
                    (Math.abs(position.x - this.prevScrollPosition.x) < 2 &&
                     Math.abs(position.y - this.prevScrollPosition.y) < 2)) {
                    this.adapter.scrollEnding();
                }

                this.prevScrollPosition.x = position.x;
                this.prevScrollPosition.y = position.y;
            }
	},

    _handleGestureStart: function(event) {
        Mojo.Log.info("Gesture start: ", event.scale);
        event.stop();
		if (this.useAdapter) {
			this.adapter.gestureStart(event.centerX, event.centerY, event.scale, event.rotation, event.centerX, event.centerY);
		}
		if (this.useMouseEvents) {
			return;
		}
        this.zoomAtGestureStart = this.currScale;
        this.offsetAtGestureStart = this.sceneScroller.getScrollPosition();
        this.offsetAtGestureStart.left = event.centerX - this.offsetAtGestureStart.left;
        this.offsetAtGestureStart.top = event.centerY - this.offsetAtGestureStart.top;
        this.inPinchZoom = true;
        this.fitWidth = false;

        if (this.pinchZoomAnimationTimer) {
			this.controller.window.clearInterval(this.pinchZoomAnimationTimer);
		}

        this.pinchZoomAnimationTimer = this.controller.window.setInterval(this._stepPinchZoomAnimation.bind(this), 35);
        this.pinchZoomCurrentScale = event.scale;
        this.pinchZoomTargetScale = event.scale;
        this.pinchZoomTargetCenter = { left: event.centerX, top: event.centerY };
        this.pinchZoomCurrentCenter = { left: event.centerX, top: event.centerY };
    },

    _handleGestureChange: function(event) {

        Mojo.Log.info("Gesture change: ", event.scale);
        event.stop();

        if (this.useAdapter && this.useMouseEvents) {
            // send gesture change events down to adapter only if we are tracking raw mouse events
            this.adapter.gestureChange(event.centerX, event.centerY, event.scale, event.rotation,
                                       event.centerX, event.centerY);
            return;
        }

        this.pinchZoomTargetScale = event.scale;
        this.pinchZoomTargetCenter = { left: event.centerX, top: event.centerY };
    },

    _handleGestureEnd: function(event) {
        Mojo.Log.info("Gesture end: ", event.scale);
        event.stop();
        this._handleGestureChange(event);
		if (this.useAdapter) {
			this.adapter.gestureEnd(event.centerX, event.centerY, event.scale, event.rotation,
                                       event.centerX, event.centerY);
		}
		if (this.useMouseEvents) {
			return;
		}

        this.inPinchZoom = false;

        this.zoomAtGestureStart = 1.0;
        this.offsetAtGestureStart = {left: 0, top: 0};

            this.controller.window.clearInterval(this.pinchZoomAnimationTimer);
            this.pinchZoomAnimationTimer = null;
    },

    _stepPinchZoomAnimation: function() {

        if (Math.abs(this.pinchZoomTargetScale - this.pinchZoomCurrentScale) < 0.001 &&
            Math.abs(this.pinchZoomTargetCenter.left - this.pinchZoomCurrentCenter.left) < 0.001 &&
            Math.abs(this.pinchZoomTargetCenter.top - this.pinchZoomCurrentCenter.top) < 0.001) {
            return;
        }

		var danielScale = 2.0 / 5.0;

        var delta = (this.pinchZoomTargetScale - this.pinchZoomCurrentScale) * danielScale;
        this.pinchZoomCurrentScale += delta;

		var scaleFactor = this.zoomAtGestureStart * this.pinchZoomCurrentScale;
		if (scaleFactor > this.maxScaleFactor) {
			// It's possible that our current scale factor is < maxScaleFactor and the new one would bring it
			// to > maxScaleFactor instead of bringing us exactly to maxScaleFactor and that leaving now
			// will leave the current scale factor at something less then maxScaleFactor. However generally we'll
			// be really close to maxScaleFactor and this is an easy solution that should be unnoticed
			// by the user.
			return;
		}

		if (this.metaViewport) {
			if (!this.metaViewport.userScalable) {
				return;
			}
			if (scaleFactor < this.metaViewport.minimumScale) {
				return;
			}
			if (scaleFactor > this.metaViewport.maximumScale) {
				return;
			}
		}

        delta = (this.pinchZoomTargetCenter.left - this.pinchZoomCurrentCenter.left) * danielScale;
        this.pinchZoomCurrentCenter.left += delta;

        delta = (this.pinchZoomTargetCenter.top - this.pinchZoomCurrentCenter.top) * danielScale;
        this.pinchZoomCurrentCenter.top += delta;

        // will this zoom level cause the zoomed width to become smaller
        // than our window width
        var scaledWidth = this.currPageWidth * scaleFactor;
        var scaledHeight = this.currPageHeight * scaleFactor;

        var eventScale = this.pinchZoomCurrentScale;
        this.currScale = scaleFactor;

        if (scaledWidth < this.controller.window.innerWidth) {
            // yes, don't change the scale factor but allow tracking to occur
            scaleFactor = this.controller.window.innerWidth / this.currPageWidth;
            scaledWidth = this.currPageWidth * scaleFactor;
            scaledHeight = this.currPageHeight * scaleFactor;

            this.currScale = scaleFactor;
            eventScale = this.currScale / this.zoomAtGestureStart;
        }

        var scrollX = this.offsetAtGestureStart.left * eventScale - this.pinchZoomCurrentCenter.left;
        var scrollY = this.offsetAtGestureStart.top * eventScale - this.pinchZoomCurrentCenter.top;

        if (scrollX < 0) {
			scrollX = 0;
		}
		else if ((scrollX + scaledWidth) < this.controller.window.innerWidth) {
			scrollX = this.controller.window.innerWidth - (scrollX + scaledWidth);
		}

        if (scrollY < 0) {
			scrollY = 0;
		}

        try {
			var lb = parseInt(this.adapter.getStyle('border-left-width'), 10);
			var rb = parseInt(this.adapter.getStyle('border-right-width'), 10);

            scaledWidth = scaledWidth + lb + rb;

			this._setDimensions(this.adapter, scaledWidth, scaledHeight);

            this.sceneScroller.scrollTo(-scrollX, -scrollY);
        }
        catch (e) {}
     },

    _handleDragStart: function(event) {
		if (this.inPinchZoom || this.useMouseEvents) {
			event.stop();
		}
    },

    _addAsScrollListener: function(event) {
        event.scroller.addListener(this);
        if (this.adapter) {
            this.adapter.scrollStarting();
        }
    },

	_handleMouseDown: function(event) {
		if (this.adapter) {
        	this.adapter.mouseEvent(this.mouseDownEventType, event.x, event.y, event.detail);
		}
    },

    _handleMouseUp: function(event) {
		if (this.adapter) {
        	this.adapter.mouseEvent(this.mouseUpEventType, event.x, event.y, event.detail);
		}
    },

    _handleMouseMove: function(event) {
		if (this.adapter) {
        	this.adapter.mouseEvent(this.mouseMoveEventType, event.x, event.y, event.detail);
		}
    },

    // scrollTo callback from the JS side
    scrollTo: function(x, y) {
        Mojo.Log.info("Scrolled to: %d x %d", x, y);
        this.sceneScroller.scrollTo(-x, -(y + this.topMargin));
        this.adapter.scrollTo(x, y);
    },

	/**
	 * Set the top margin (in pixels)
	 * @param {Number} margin
	 */
	setTopMargin: function(margin) {

		this.topMargin = margin;
	},

	/**
	 * Called by the adapter whenever a URL has been redirected.
	 */
	urlRedirected: function(url, userData) {

		url = this.$X(url);

		Mojo.Log.info("Got URL redirect: '%s' -> '%s'", url, userData);
        Mojo.Event.send(this.adapter, Mojo.Event.webViewUrlRedirect,
			{url: url, appId: userData});
	},

	_addRedirects: function(table, type, skipAppId) {
		for (var i = 0; i < table.length; i++) {

			var entry = table[i];

			if (skipAppId && skipAppId == entry.appId) {
				continue;
			}

			try {
				this.addUrlRedirect(entry.url, true, entry.appId, type);
			}
			catch (e) {
				Mojo.Log.logException(e, "Failure adding redirect rule '%s' -> '%s'", entry.url, entry.appId);
			}
		}
	},

	/**
	 * Read the command resource handler table and send down redirect handler
	 * commands to the browser server.
	 */
	addSystemRedirects: function(skipAppId) {
		var restable = Mojo.loadJSONFile("/usr/palm/command-resource-handlers.json");
		this._addRedirects(restable.redirects, 0, skipAppId);
		this._addRedirects(restable.commands, 1, skipAppId);
	},

    // pageDimensions callback from the JS side
    pageDimensions: function(width, height) {

        //Mojo.Log.info("Page dimensions changed: %d x %d", width, height);
		if (Date.now() < this.handlePageDimensionsAfter) {
			return;
		}

		if (width === 0 || height === 0) {

			// reset fit width and scroll positions. FIXME: use history when its ready
			this.currScale = 1.0;
			this.currPageWidth = 0;
			this.currPageHeight = 0;
			delete this.metaViewport;
			this._clearAutoZoomInfo();
			this.sceneScroller.scrollTo(0, -this.topMargin);

			this._setDimensions(this.adapter, this.controller.window.innerWidth, this.controller.attributes.virtualpageheight);
			return;
		}

        width = width > this.controller.window.screen.width ? width : this.controller.window.screen.width;
        height = height > this.controller.attributes.minimumpageheight ? height :
			this.controller.attributes.minimumpageheight;
		this.currPageWidth = width;
		this.currPageHeight = height;

		if (this.metaViewport) {
			var minScale = this.controller.window.innerWidth / width;
			this.currScale = Math.max(this.metaViewport.initialScale, minScale);

			if (this.useAdapter) {
				this.adapter.setMagnification(this.currScale);
			}
		}
		else if (this.fitWidth) {
		 	this.currScale = this.controller.window.innerWidth / width;
			if (this.useAdapter) {
				this.adapter.setMagnification(this.currScale);
			}
		}

		this._setDimensions(this.adapter,
				this.currPageWidth * this.currScale,
				this.currPageHeight * this.currScale);
    },

	metaViewportSet: function(initialScale, minimumScale, maximumScale,
							  width, height, userScalable) {
		Mojo.Log.info("MetaViewport: scale: %d, %d, %d, dims: %d, %d, userScalable: %d",
					  initialScale, minimumScale, maximumScale,
					  width, height, userScalable);

		this.metaViewport = new Mojo.WebView.MetaViewport(initialScale, minimumScale, maximumScale,
														  width, height, userScalable);

		this.currScale = initialScale;
		if (this.useAdapter) {
			this.adapter.setMagnification(this.currScale);
		}

		this._setDimensions(this.adapter,
							this.currPageWidth * this.currScale,
							this.currPageHeight * this.currScale);
	},

    titleURLChange : function(title, url, canGoBack, canGoForward) {

		title = this.$X(title);
		url = this.$X(url);

		this.isAutoZoomed = false;
		this.lastUrl = url;
        Mojo.Event.send(this.adapter, Mojo.Event.webViewTitleUrlChanged,
			{title: title, url: url, canGoBack:canGoBack, canGoForward:canGoForward});
    },

	titleChanged: function(title) {

		title = this.$X(title);

		this.isAutoZoomed = false;
		Mojo.Event.send(this.adapter, Mojo.Event.webViewTitleChanged, {title: title});
    },

	urlChange: function(url, canGoBack, canGoForward) {

		url = this.$X(url);

		this.lastUrl = url;
		this.isAutoZoomed = false;
		Mojo.Event.send(this.adapter, Mojo.Event.webViewUrlChanged,
			{url: url, canGoBack:canGoBack, canGoForward:canGoForward});
    },

    smartZoomCalculateResponseSimple: function(left, top, right, bottom, centerX, centerY) {

		try {
			//Mojo.Log.info('Smart Zoom response pt:(%d,%d) r: L:%d, T:%d, R:%d, B:%d', centerX, centerY, left, top, right, bottom);

			var rectW = right - left;
			var rectH = bottom - top;

			if (rectW <= 0 || rectH <= 0) {
				Mojo.Log.info("Smart zoom failed");
				return;
			}

			var newScale = this.controller.window.innerWidth / rectW;
			if (this.metaViewport) {
				if (!this.metaViewport.userScalable) {
					return;
				}
				if (newScale < this.metaViewport.minimumScale) {
					return;
				}
				if (newScale > this.metaViewport.maximumScale) {
					return;
				}
			}

			var newZoomRectangle = new Mojo.WebView.Rectangle(left, top, right, bottom);

			var marginX = (rectW * this.smartZoomScaleSlopFactor - rectW) / 2;
			var marginY = (rectH * this.smartZoomScaleSlopFactor - rectH) / 2;

			left -= marginX;
			right += marginX;
			top -= marginY;
			bottom += marginY;

			left = Math.max(left, 0);
			top = Math.max(top, 0);
			right = Math.min(right, this.currPageWidth);
			bottom = Math.min(bottom, this.currPageHeight);

			newScale = this.controller.window.innerWidth / (right - left);

			var zoomLeft = Math.floor(left * newScale);
			
			var currScrollPos = this.sceneScroller.getScrollPosition();
			var currScrollLeft = -currScrollPos.left;

			// comparing left & scale is equal to comparing left & right
			if (this.lastZoomRectangle && this.lastZoomRectangle.equals(newZoomRectangle) &&
				currScrollLeft === zoomLeft && newScale === this.currScale) {

				// Same zoom results as before and we're at the same horizontal position so fit to page.

				// Calculate the current page center before anything is changed.
				var sceneCenter = currScrollPos;
				sceneCenter.left -= this.prevWindowSize.width / 2.0;
				sceneCenter.top -= this.prevWindowSize.height / 2.0;
				var pageCenter = this._sceneToPage(sceneCenter);

				this.currScale = this.controller.window.innerWidth / this.currPageWidth;

				// Resize page back to fit screen width
				this._setDimensions(this.adapter, this.currScale * this.currPageWidth, this.currScale * this.currPageHeight);

				if (this.useAdapter) {
					this.adapter.setMagnification(this.currScale);
				}

				// Scroll so that the previous center is still in the center of the screen.
				var scenePos = this._pageToScene(pageCenter);
				scenePos.left += this.controller.window.innerWidth / 2.0;
				scenePos.top += this.controller.window.innerHeight / 2.0;

				this.sceneScroller.scrollTo(scenePos.left, scenePos.top);

				this._clearAutoZoomInfo();
			}
			else {
				// we need to fit horizontally and center around the Y location
				this.fitWidth = false;
				var autoZoomTop = centerY * newScale - this.controller.window.innerHeight / 2;

				this.currScale = newScale;
				this._setDimensions(this.adapter, this.currPageWidth * this.currScale, this.currPageHeight * this.currScale);

				this.sceneScroller.scrollTo(-zoomLeft, -autoZoomTop);

				this.lastZoomRectangle = newZoomRectangle;
			}
		}
		catch (e) {
			Mojo.Log.logException(e, 'smartZoomCalculateResponseSimple');
		}
    },

	downloadFinished: function(url, mimeType, tmpFilePath) {

		url = this.$X(url);

        Mojo.Log.info('Download finished');
		Mojo.Event.send(this.adapter, Mojo.Event.webViewDownloadFinished,
			{url: url, mimeType: mimeType, tmpFilePath:tmpFilePath});
    },

	/*
	 * Send the response once an Alert has been dismissed
	 */
	sendDialogResponse: function(arg1, arg2, arg3) {
		if (arg3) {
			this.adapter.sendDialogResponse(arg1, arg2, arg3);
		}
		else if (arg2) {
			this.adapter.sendDialogResponse(arg1, arg2);
		}
		else {
			this.adapter.sendDialogResponse(arg1);
		}
	},

	/**
	 * Called by the adapter when it's time to update the global history
	 * @param {String} url
	 * @param {Boolean} reload
	 */
	updateGlobalHistory: function(url, reload) {

		url = this.$X(url);

		Mojo.Event.send(this.adapter, Mojo.Event.webViewUpdateHistory,
			{url: url, reload:reload});
	},

	/*
	 * Show a Javascript alert dialog
	 */
	dialogAlert: function(msg) {

		msg = this.$X(msg);
		var response = function(arg1) {
			try {
				this.sendDialogResponse(arg1);
			}
			catch (e) {
				Mojo.Log.logException(e, 'dialogAlert');
			}
			this.popupWidgetElement = null;
			this._notifyPopupListeners('close');
		}.bind(this);

		// Don't display the dialog unless the app is full screen
		if (!this.hasFocus) {
			this.sendDialogResponse("0");
			return;
		}

		this.popupWidgetElement = this.controller.scene.showAlertDialog({
			onChoose: function(value) {response("1");},
        	message: msg,
        	choices:[{label:$LL('OK'), value:'1', type:'dismiss'}]
    	});

		this._notifyPopupListeners('open');
	},

	/*
	 * Show a Javascript confirmation dialog
	 */
	dialogConfirm: function(msg) {

		msg = this.$X(msg);

		var response = function(arg1, arg2) {
			try {
				this.sendDialogResponse(arg1, arg2);
			}
			catch (e) {
				Mojo.Log.logException(e, 'dialogConfirm');
			}
			this.popupWidgetElement = null;
			this._notifyPopupListeners('close');
		}.bind(this);

		// Don't display the dialog unless the app is full screen
		if (!this.hasFocus) {
			this.sendDialogResponse("0");
			return;
		}

		this.popupWidgetElement = this.controller.scene.showAlertDialog({
			onChoose: function(value) {
			if (value) {
				response(value);
			}
			else {
				response("1");
			}},
        	message: msg,
        	choices:[
				{label:$LL('OK'), value:'1', type:'affirmative'},
				{label:$LL('Cancel'), value:'0', type:'dismiss'}]
    	});

		this._notifyPopupListeners('open');
	},

	popupChoose: function(value) {
		try {
			this.adapter.selectPopupMenuItem(this.popupMenuId, parseInt(value, 10));
		}
		catch (e) {
			Mojo.Log.logException(e, 'popupChoose');
		}

		this.popupWidgetElement = null;
		this._notifyPopupListeners('close');
	},

	showPopupMenu: function(menuId, menuObj) {
		try {
			this.popupMenuId = menuId;
			var menuData = menuObj.evalJSON();

			var items = [];
			for (var i = 0; i < menuData.items.length; i++) {
				items.push({
					label: menuData.items[i].text,
					command: i.toString(), // FIXME No longer have to convert to string now that NOV-29375 is fixed
					disabled: !menuData.items[i].isEnabled
				});
			}

			this.popupWidgetElement = this.controller.scene.popupSubmenu({
				onChoose:this.popupChoose.bind(this),
				toggleCmd: items[menuData.selectedIdx].command,
				items: items});

			this._notifyPopupListeners('open');
		}
		catch (e) {
			Mojo.Log.logException(e, 'showPopupMenu');
		}
	},

	hidePopupMenu: function(menuId) {
		// I don't believe that the way we implement popup menus (i.e. they are modal)
		// that we can ever encounter a situation where we are asked to hide a popup
		// menu. Will implement if this turns out to be false.
	},

	/**
	 * Show a Javascript prompt
	 */
	dialogPrompt: function(msg, defaultValue) {

		msg = this.$X(msg);
		defaultValue = this.$X(defaultValue);

		var data = {};
		var response = function(arg1, arg2, arg3) {
			try {
				this.sendDialogResponse(arg1, arg2, arg3);
			}
			catch (e) {
				Mojo.Log.logException(e, 'dialogPrompt');
			}
			this.popupWidgetElement = null;
			this._notifyPopupListeners('close');

		}.bind(this);

		// Don't display the dialog unless the app is full screen
		if (!this.hasFocus) {
			this.sendDialogResponse("0");
			return;
		}

		// Set up template values
		data.prompt = msg;
		data.value = defaultValue;
		data.displayPassword = 'display:none';
		data.template = Mojo.Widget.getSystemTemplatePath('webview/webview-prompt');

		// Show the prompt
		data.assistant = new Mojo.Widget.WebView.DialogPromptAssistant(this, data, response);
		this.popupWidgetElement = this.controller.scene.showDialog(data);
		if (this.popupWidgetElement) {
			this._notifyPopupListeners('open');
		}
	},

	/**
	 * Prompt the user for their username and password
	 */
	dialogUserPassword: function(msg) {

		msg = this.$X(msg);

		var data = {};
		var response = function(arg1, arg2, arg3) {
			try {
				this.sendDialogResponse(arg1, arg2, arg3);
			}
			catch (e) {
				Mojo.Log.logException(e, 'dialogUserPassword');
			}
			this.popupWidgetElement = null;
			this._notifyPopupListeners('close');
		}.bind(this);

		// Don't display the dialog unless the app is full screen
		if (!this.hasFocus) {
			this.sendDialogResponse("0");
			return;
		}

		// Set up template values
		data.prompt = (msg === undefined) ? '<no message>' : msg;
		data.value = '';
		data.displayPassword = '';
		data.isPasswordPrompt = true;
		data.template = Mojo.Widget.getSystemTemplatePath('webview/webview-prompt');

		// Show the username & password prompt
		data.assistant = new Mojo.Widget.WebView.DialogPromptAssistant(this, data, response);
		this.popupWidgetElement = this.controller.scene.showDialog(data);
		if (this.popupWidgetElement) {
			this._notifyPopupListeners('open');
		}
	},

	linkClicked : function(url) {

		url = this.$X(url);

		// Take the stored tap event for this tap and send onto our listeners.
		var tmpEvent = Mojo.Model.decorate(this._lastTapEvent.up, {url: url});
		Mojo.Event.send(this.adapter, Mojo.Event.webViewLinkClicked, tmpEvent);
		delete this._lastTapEvent;
	},

	firstPaintComplete: function() {
		Mojo.Event.send(this.adapter, Mojo.Event.webViewFirstPaintComplete, {});
	},

	/**
	 * Called by the adapter when the load progress changes.
	 *
	 * @private
	 */
	loadProgress: function(progress) {
        Mojo.Event.send(this.adapter, Mojo.Event.webViewLoadProgress, {'progress': progress});
    },

	_saveCurrentPagePosition: function() {
		var pos = this.sceneScroller.getScrollPosition();
		var dims = this.adapter.getDimensions();
		var item = this.historyInfo[this.lastUrl];
		if (!item) {
			item = new Mojo.WebView.HistoryItem();
			this.historyInfo[this.lastUrl] = item;
		}
		item.scrollX = pos.left;
		item.scrollY = pos.top;
		item.height = dims.height;
		item.width = dims.width;
		item.zoom = this.currScale;
		item.posSet = true;
		this.currentPagePositionSaved = true;
	},

	_setDimensions: function(element, width, height) {
		element.style.width = width + 'px';
		element.style.height = height + 'px';
	},

	_restoreCurrentPagePosition: function() {
		var item = this.historyInfo[this.lastUrl];
		if (item && item.posSet) {

			this.currScale = item.zoom;

			this.adapter.setMagnification(item.zoom);

			this._setDimensions(this.adapter, item.width, item.height);

			this.sceneScroller.scrollTo(item.scrollX, item.scrollY);

			this.handlePageDimensionsAfter = Date.now() + 1000;	// FIXME Hack
		}
	},

	loadStarted: function() {
        try {
			if (!this.currentPagePositionSaved) {
				this._saveCurrentPagePosition();
			}
			this.currentPagePositionSaved = false;
			Mojo.Event.send(this.adapter, Mojo.Event.webViewLoadStarted, {});
		}
		catch (e) {
			Mojo.Log.logException(e, 'loadStarted');
		}
    },

	loadStopped: function() {
		try {
			// Only restore the page position if fitWidth is in use.
			if (this.fitWidth) {
				this._restoreCurrentPagePosition();
			}
			Mojo.Event.send(this.adapter, Mojo.Event.webViewLoadStopped, {});
		}
		catch (e) {
			Mojo.Log.logException(e, 'loadStopped');
		}
    },

	didFinishDocumentLoad: function() {
		try {
	        Mojo.Event.send(this.adapter, Mojo.Event.webViewDidFinishDocumentLoad, {});
		}
		catch (e) {
			Mojo.Log.logException(e, 'didFinishDocumentLoad');
		}
    },

	failedLoad: function(domain, errorCode, failingURL, localizedMessage) {

		// TODO: Do all these need to be protected, including the errorCode?
		domain = this.$X(domain);
		failingURL = this.$X(failingURL);
		localizedMessage = this.$X(localizedMessage);

        Mojo.Event.send(this.adapter, Mojo.Event.webViewLoadFailed, {domain: domain, errorCode: errorCode,
			failingURL: failingURL, message: localizedMessage});
	},

	purgePage: function() {
		Mojo.Log.warn("Have been requested to purge the browser page (%s).", this.lastUrl);
		if (!this.hasFocus) {
			// Adapter will send event to notify us that it's been disconnected.
			this.adapter.disconnectBrowserServer();
		}
	},

	/**
	 * Called when the BrowserServer cannot handle a given URL. This is called when
	 * WebKit cannot support the URL scheme.
	 *
	 * @param {String} mime The mime type (usually unknown or empty)
	 * @param {String} url The associated URL.
	 */
	mimeNotSupported: function(mime, url) {

		url = this.$X(url);

		Mojo.Log.info("Mime not supported: %s (%s)", url, mime);
        Mojo.Event.send(this.adapter, Mojo.Event.webViewMimeNotSupported, {url: url, mimeType: mime});
	},

	/**
	 * Called when the BrowserServer cannot (or isn't supposed to) handle a mime type.
	 * Will only be called for the main resource URL.
	 *
	 * @param {String} mime The mime type.
	 * @param {String} url The associated URL.
	 */
	mimeHandoffUrl: function(mime, url) {

		url = this.$X(url);

		Mojo.Log.info("Mime handoff: %s (%s)", url, mime);
        Mojo.Event.send(this.adapter, Mojo.Event.webViewMimeHandoff, {url: url, mimeType: mime});
	},

	_setAdapterConnectionState: function(connected) {
		this.adapterConnected = connected;
		if (connected) {
			if (this._serverConnectTimer !== undefined) {
				this.controller.window.clearTimeout(this._serverConnectTimer);
				delete this._serverConnectTimer;
			}
		}
		else {
			if (this._serverConnectTimer === undefined) {
				this._serverConnectTimer = this.controller.window.setTimeout(this._connectAdapterToServer, 3000);
			}
		}
	},

	_connectAdapterToServer: function() {

		try {
			delete this._serverConnectTimer;
			this.adapter.connectBrowserServer();
			this._setAdapterConnectionState(true);

			Mojo.Event.send(this.adapter, Mojo.Event.webViewServerConnect, {});
		}
		catch (e) {
			this._setAdapterConnectionState(false);
		}
	},

	browserServerDisconnected: function() {
		try {
			Mojo.Log.error("Disconnected from BrowserServer.");
			if (this.hasFocus) {
				// Only the foreground widget will try to reconnect.
				this._setAdapterConnectionState(false);
			}
			else {
				// else set state try again once the application is brought to the foreground.
				this.adapterConnected = false;
			}
			Mojo.Event.send(this.adapter, Mojo.Event.webViewServerDisconnect, {});
		}
		catch (e) {
			Mojo.Log.logException(e, 'browserServerDisconnected');
		}
	},

	setMainDocumentError: function(domain, errorCode, failingURL, localizedMessage) {

		// TODO: Do all these need to be protected, including the errorCode?
		domain = this.$X(domain);
		failingURL = this.$X(failingURL);
		localizedMessage = this.$X(localizedMessage);

        Mojo.Event.send(this.adapter, Mojo.Event.webViewSetMainDocumentError, {domain: domain, errorCode: errorCode,
			failingURL: failingURL, message: localizedMessage});
	},

	editorFocused: function(focused) {
        Mojo.Event.send(this.adapter, Mojo.Event.webViewEditorFocused, {focused: focused});
	},

	reportError: function(url, code, message) {

		url = this.$X(url);
		message = this.$X(message);

		Mojo.Log.error("Error %d on page: '%s', msg: '%s'", code, url, message);
		// How best to report this to our client.
	},

	createPage: function(pageIdentifier) {
	    Mojo.Event.send(this.adapter, Mojo.Event.webViewCreatePage, {'pageIdentifier': pageIdentifier});
	},

    clickRejected: function(tapIndex) {
		delete this._lastTapEvent;
        if (tapIndex >= this.tapIndex) {
            Mojo.Event.send(this.adapter, Mojo.Event.webViewTapRejected, {});
        }
    },

	/**
	 * Called after the browser adapter is successfully initialized.
	 */
	adapterInitialized: function() {
		Mojo.Log.info("Browser adapter initialized.");
		try {
            // NOTE ** no BrowserAdapter calls before this.adapter.setPageIdentifier
	        // setPageIdentifier *needs* to be the first adapter call
	        // Otherwise, the default identifier, -1, will be used in the case
	        // when the BrowserAdapter needs to pair with an existing
	        // BrowserPage on the BS side, which will result in a leaked BP
	        // and a blank page.
	        if (this.controller.attributes.pageIdentifier !== undefined) {
	            this.adapter.setPageIdentifier(this.controller.attributes.pageIdentifier);
	        }
	        else if (this.controller.attributes.url !== undefined) {
	            this.openURL(this.controller.attributes.url);
	        }
	        if (this.controller.attributes.minFontSize !== undefined) {
	            this.adapter.setMinFontSize(this.controller.attributes.minFontSize);
	        }

			if (this.controller.attributes.interrogateClicks) {
				this.adapter.interrogateClicks(true);
			}

			if (this.controller.attributes.showClickedLink !== undefined) {
			    this.adapter.setShowClickedLink(this.controller.attributes.showClickedLink);
			}

	        this.adapter.setViewportSize(this.controller.window.innerWidth, this.controller.attributes.virtualpageheight);
		}
		catch (e) {
			Mojo.Log.logException(e, 'adapterInitialized');
		}
	},

	registerOnPopup: function(callback) {

		this.popupCallbacks.push(callback);
	},

	unregisterOnPopup: function(callback) {

		var idx = this.popupCallbacks.indexOf(callback);
		if (idx >= 0) {
			this.popupCallbacks[idx] = null;
			this.popupCallbacks = this.popupCallbacks.compact();
		}
	},

	focus: function() {
	    this.adapter.focus();
	},

	blur: function() {
	    this.adapter.blur();
	},

	setShowClickedLink: function(enable) {
	    if (this.useAdapter) {
		this.adapter.setShowClickedLink(enable);
	    }
	},

	_notifyPopupListeners: function(state) {

		Mojo.Log.info("WebView#_notifyPopupListeners(%s)", state);
		for (var idx=0, len=this.popupCallbacks.length; idx < len; idx++) {
			this.popupCallbacks[idx](state);
		}
	},

	/**
	 * Show a Certificate warning prompt
	 */
	dialogSSLConfirm: function(host,code) {

		host = this.$X(host);

		var response = function(result) {

			try {
				this.sendDialogResponse(result);
			} catch (e) {
				Mojo.Log.logException(e, 'dialogCertificateWarning()');
			}
			this.popupWidgetElement = null;
			this._notifyPopupListeners('close');

		}.bind(this);

		// If we don't have focus then do not display the dialog and auto respond.
		if (!this.hasFocus) {
			this.sendDialogResponse('0');
			return;
		}

		// Show the certificate dialog
		this.popupWidgetElement = this.controller.scene.showDialog({
				certReason: Mojo.Widget.WebView.CertificateErrors.getCertificateErrorString({code: code, websiteName: host}),

				// WARNING: MUST CHANGE WHEN INSTALLED IN THE FRAMEWORK.
				template: Mojo.Widget.getSystemTemplatePath('webview/certwarn-dialog'),
				assistant: new Mojo.WebView.CertificateWarningAssistant(this.controller, response)
			});
		if (this.popupWidgetElement) {
			this._notifyPopupListeners('open');
		}
	}
});


/**
 * Certificate Warning dialog
 */
Mojo.WebView.CertificateWarningAssistant = Class.create({

	initialize: function(controller, sendResponse) {

		this._sendResponse = sendResponse;
		this.controller = controller;
		this._certTrustHandler     = this._certTrust.bindAsEventListener(this);
		this._certTrustOnceHandler = this._certTrustOnce.bindAsEventListener(this);
		this._certTrustDontHandler = this._certTrustDont.bindAsEventListener(this);
	},

	setup: function(widget) {

		this.widget = widget;

		// Listen for button taps
		Mojo.Event.listen(this.controller.get('cert_trust_button'), Mojo.Event.tap, this._certTrustHandler, true);
		Mojo.Event.listen(this.controller.get('cert_trustonce_button'), Mojo.Event.tap, this._certTrustOnceHandler, true);
		Mojo.Event.listen(this.controller.get('cert_trustdont_button'), Mojo.Event.tap, this._certTrustDontHandler, true);
	},

	cleanup: function() {

		Mojo.Event.stopListening(this.controller.get('cert_trust_button'), Mojo.Event.tap, this._certTrustHandler, true);
		Mojo.Event.stopListening(this.controller.get('cert_trustonce_button'), Mojo.Event.tap, this._certTrustOnceHandler, true);
		Mojo.Event.stopListening(this.controller.get('cert_trustdont_button'), Mojo.Event.tap, this._certTrustDontHandler, true);

		// If we are closed by any other method than a button press then
		// send a 'don't trust' response.
		if (this._sendResponse) {
			this._sendResponse('0');
		}
	},

	_certTrust: function() {

		this._sendResponse('1');
		delete this._sendResponse;
		// Close the dialog box
		this.widget.mojo.close();
	},

	_certTrustOnce: function() {

		this._sendResponse('2');
		delete this._sendResponse;
		// Close the dialog box
		this.widget.mojo.close();
	},

	_certTrustDont: function() {

		this._sendResponse('0');
		delete this._sendResponse;
		// Close the dialog box
		this.widget.mojo.close();
	}
});

/**
 * Assistant for the prompt and username/password dialog
 */
Mojo.Widget.WebView.DialogPromptAssistant = Class.create({

	initialize: function(widgetController, data, sendCb) {
		this.data = data;
		this.sendCb = sendCb;
		this.controller = widgetController.controller;
		this.okButtonHandler = this.handleOkayButton.bindAsEventListener(this);
		this.cancelHandler = this.handleCancelButton.bindAsEventListener(this);
	},

	setup : function(widget) {
		this.widget = widget;
		// Listen for button taps
		Mojo.Event.listen(this.controller.get('webViewDialogOkayButton'), Mojo.Event.tap, this.okButtonHandler, true);
		Mojo.Event.listen(this.controller.get('webViewDialogCancelButton'), Mojo.Event.tap, this.cancelHandler, true);
		// Select and set focus on the prompt or username
		var dialogTextElement = this.controller.get('webViewDialogText');

		Mojo.Log.info("DialogPromptAssistant#setup() -> dialogTextElement= " + dialogTextElement);
		dialogTextElement.select();
		dialogTextElement.focus();
	},

	handleOkayButton: function() {
		var str = this.controller.get('webViewDialogText').value;
		var pwd = this.controller.get('webViewDialogPassword').value;
		Mojo.Log.info("dialogPromptAssistant: Okay tapped. str= %s pwd=%s", str, pwd);
		// Call the callback now
		if (this.data.isPasswordPrompt) {
			this.sendCb("1", str, pwd);
		}
		else {
			this.sendCb("1", str);
		}

		// Clear the callback so it isn't called by the cleanup routine
		delete this.sendCb;

		// Close the dialog box
		this.widget.mojo.close();
	},

	handleCancelButton: function() {
		Mojo.Log.info("dialogPromptAssistant: Cancel tapped");

		// Send "Cancel"
		this.sendCb('0');

		// Clear the callback so it isn't called by the cleanup routine
		delete this.sendCb;

		// Close the dialog box
		this.widget.mojo.close();
	},

	cleanup: function() {
		Mojo.Log.info("dialogPromptAssistant: Cleanup");
		Mojo.Event.stopListening(this.controller.get('webViewDialogOkayButton'), Mojo.Event.tap, this.okButtonHandler, true);
		Mojo.Event.stopListening(this.controller.get('webViewDialogCancelButton'), Mojo.Event.tap, this.cancelHandler, true);

		// Send "Cancel" if necessary
		if (this.sendCb) {
			this.sendCb('0');
		}
	}
});

Mojo.Widget.WebView.CertificateErrors = function() {

	var codes = {
		'0': $L("The security certificate #{websiteName} sent is expired. Connecting to this site might put your confidential information at risk."),
		'2': $L("The website #{websiteName} didn't send a security certificate to identify itself. Connecting to this site might put your confidential information at risk."),
		'5': $L("The security certificate #{websiteName} sent could not be read completely. Connecting to this site might put your confidential information at risk."),
		'10': $L("The security certificate #{websiteName} sent has some invalid information. Connecting to this site might put your confidential information at risk."),
		'18': $L("The security certificate #{websiteName} sent has questionable signatures. Connecting to this site might put your confidential information at risk."),
		'24': $L("The security certificate #{websiteName} sent is invalid. Connecting to this site might put your confidential information at risk."),
		'30': $L("The security certificate #{websiteName} sent has inconsistent information in it. Connecting to this site might put your confidential information at risk.")
	};

	var table = $H();
	table.set('0', codes['0']);

	table.set('2', codes['2']);
	table.set('3', codes['2']);
	table.set('4', codes['2']);

	table.set('5', codes['5']);
	table.set('6', codes['5']);
	table.set('7', codes['5']);
	table.set('8', codes['5']);
	table.set('9', codes['5']);

	table.set('10', codes['10']);
	table.set('11', codes['10']);
	table.set('12', codes['10']);
	table.set('13', codes['10']);
	table.set('14', codes['10']);
	table.set('15', codes['10']);
	table.set('16', codes['10']);
	table.set('17', codes['10']);

	table.set('18', codes['18']);
	table.set('19', codes['18']);
	table.set('20', codes['18']);
	table.set('21', codes['18']);
	table.set('22', codes['18']);
	table.set('23', codes['18']);

	table.set('24', codes['24']);
	table.set('25', codes['24']);
	table.set('26', codes['24']);
	table.set('27', codes['24']);
	table.set('28', codes['24']);
	table.set('29', codes['24']);

	table.set('30', codes['30']);
	table.set('31', codes['30']);
	table.set('50', codes['30']);

	return {
		getCertificateErrorString: function(model){
			if (model.code !== undefined) {
				var templateString = table.get(model.code.toString());
				if (templateString) {
					var template = new Template(templateString);
					return template.evaluate(model);
				}
			}
		}
	};
}();
