(function($) {
    var toolSet = {
        'bold': { name: 'Bold', command: 'bold', value: '', key: 'b' },
        'italic': { name: 'Italics', command: 'italic', value: '', key: 'i' },
        'underline': { name: 'Underline', command: 'underline', value: '', key: 'u' },
        'ul': { name: 'Unordered List', command: 'insertunorderedlist', value: '' },
        'ol': { name: 'Ordered List', command: 'insertorderedlist', value: '' },
        'link': { name: 'Insert link', command: 'link' },
        'image': { name: 'Insert image' },
        'viewSource': { name: 'View Source' },
        'sep': false
    };
    
    var BssEditor = function (id, settings)
    {
        settings = $.extend({
			'base': '',
            'toolbar': ['bold', 'italic', 'underline', 'sep', 'ul', 'ol', 'sep', 'link', 'image', 'sep', 'viewSource'],
            'width': 'auto',
            'css': 'html, body { background: #fff; font-family: Arial, "sans serif"; font-size: 100%; }'
        }, settings);
        
        var elt = $('#'+id);
        
        if (settings.width == 'auto')
        {
            settings.width = (elt.hasClass('full-length') ? '100%' : elt.width() + 'px');
        }
        
        this.id = id;
        this.wyid = 'wysiwyg-' + id;
        this.wysiwyg = true;
        this.keyBindings = {};
        this.eventListeners = {};
        
        var w = settings.width;
        
        var ul = document.createElement('ul');
        ul.className = 'wysiwyg-toolbar';
        ul.id = this.wyid + '-toolbar';
        
        var iframe = document.createElement('iframe');
        iframe.src = 'about:blank';
        iframe.id = iframe.name = this.wyid;
        iframe.className = 'wysiwyg-iframe';
        
        var clear = document.createElement('div');
        clear.style.clear = 'left';
        
        $('#' + id).hide().before([ul, clear, iframe]);
        $(iframe)
            .height(elt.height())
            .add(ul).width(w); // Set width of iframe and ul.
        
        var doc = this.getDocument();
        doc.open();
        doc.write('<!DOCTYPE html><html><head>' + (settings.base ? '<base href="' + settings.base + '">' : '') + '<meta http-equiv="Content-Type" content="text/html; charset=utf-8"><style type="text/css">' + settings.css + '</style><title>This is how the sausage is made.</title></head><body></body></html>');
        doc.close();
        doc.designMode = 'on';
        this.configureDesignMode();
        
		this.originalContent = $('#' + id).val();
        
        var toolKey, tool;
        var self = this;
        var handler;
        var hover = function () { $(this).addClass('active'); }
        var unhover = function () { $(this).removeClass('active'); }
        
        if (this.originalContent)
        {
            window.setTimeout(function () { self.insertHtml(self.originalContent); }, 100);
        }
        
        for (var i = 0; i < settings.toolbar.length; i++)
        {
            toolKey = settings.toolbar[i];
            tool = toolSet[toolKey];
            
            if (!tool)
            {
                $('<li class="wysiwyg-toolbar-tool wysiwyg-toolbar-sep">&nbsp;</li>').appendTo(ul);
            }
            else
            {
                handler = this.clickHandler(toolKey);
                
                $('<li class="wysiwyg-toolbar-tool wysiwyg-toolbar-' + toolKey + '" id="' + this.wyid + '-toolbar-' + toolKey + '"><a href="javascript:void(null)" tabindex="-1">' + tool.name + '</a></li>')
                    .mousedown(function (evt) {
                        var doc = self.getDocument();
                        
                        // IE forgets the selection within the iframe when
                        // something else on the page is clicked. This means
                        // our buttons wouldn't work because they wouldn't
                        // know where to insert the HTML they generated. To
                        // fix this, we remember the selection when the
                        // button is clicked and restore it later.
                        
                        if (doc.selection)
                        {
                            self.restoreRange = doc.selection.createRange();
                        }
                        
                        evt.preventDefault();
                        evt.stopPropagation();
                        return false;
                    })
                    .click(handler)
                    .children('a')
                        .mouseover(hover).mouseout(unhover)
                        .focus(hover).blur(unhover)
                        .click(handler)
                        .end()
                    .appendTo(ul);
                
                if (tool.key)
                {
                    // Setup key binding.
                    this.keyBindings[tool.key] = toolKey;
                }
            }
        }
        
        $(doc).keypress(function (evt) {
            if (evt.ctrlKey)
            {
                var key = String.fromCharCode(evt.which);
                
                if (self.keyBindings[key])
                {
                    self.execCommand(self.keyBindings[key]);
                    evt.preventDefault();
                    evt.stopPropagation();
                }
            }
            
            self.checkActiveStates();
        });
        
        $(doc).mouseup(function () {
            self.checkActiveStates();
        });
        
        // Hook up form submission.
		elt.parents('form').submit(function () { self.addSubmitElement(this); });
    };
    
    BssEditor.prototype.configureDesignMode = function ()
    {
        var doc = this.getDocument();
        try { doc.execCommand('useCSS', false, true); doc.execCommand('styleWithCSS', false, false); }
        catch (e) {}
    };

	BssEditor.prototype.hasChanged = function ()
	{
		return (this.originalContent != this.getContent());
	};
    
    BssEditor.prototype.checkActiveStates = function ()
    {
        var doc = this.getDocument();
        var ul = document.getElementById(this.wyid + '-toolbar');
        var tool;
        
        for (var toolKey in toolSet)
        {
            tool = toolSet[toolKey];
            
            if (tool && tool.command)
            {
                if (this.queryCommandState(tool.command))
                {
                    $('.wysiwyg-toolbar-' + toolKey, ul).addClass('active');
                }
                else
                {
                    $('.wysiwyg-toolbar-' + toolKey, ul).removeClass('active');
                }
            }
        }
    };

	BssEditor.prototype.queryCommandState = function (command)
	{
		var doc = this.getDocument();
		
		if (command == 'link')
		{
			return (this.getSelectionAncestor('a') != null);
		}
		
		return doc.queryCommandState(command);
	};
	
	BssEditor.prototype.getSelectionAncestor = function (selector)
	{
		var parents = $(this.getSelectedNode()).parents(selector);
		return (parents.length > 0 ? parents[0] : null);
	};
    
    BssEditor.prototype.clickHandler = function (toolKey)
    {
        var self = this;
        
        return function (evt) {
            self.getWindow().focus();
            
            if (self.restoreRange)
            {
                self.restoreRange.select();
            }
            
            self.execCommand(toolKey);
            self.checkActiveStates();
            evt.preventDefault();
            evt.stopPropagation();
        };
    };
    
    BssEditor.prototype.getWindow = function ()
    {
        return document.getElementById(this.wyid).contentWindow;
    };
    
    BssEditor.prototype.getDocument = function ()
    {
        return this.getWindow().document;
    };
    
    BssEditor.prototype.execCommand = function (toolKey)
    {
        if (!this.fireEvent('execCommand', toolKey))
        {
            var tool = toolSet[toolKey];
            var win = this.getWindow();
            var doc = this.getDocument();
            var self = this;
            
            switch (toolKey)
            {
                case 'viewSource':
                    $('#' + this.wyid + '-toolbar .wysiwyg-toolbar-viewSource').toggleClass('active');
                    this.setWysiwyg(!this.wysiwyg);
                    break;
                
                case 'link':
                    this.createLinkOverlay();
                    break;
                
                case 'image':
                    break;
                
                default:
                    doc.execCommand(tool.command, false, tool.value);
                    win.focus();
                    break;
            }
        }
    };

	/**
	 * Browsers like to change URLs in the 'href'/'src' attributes of HTML
	 * inserted into the editor. The editor side steps this issue by adding
	 * redundant attributes named 'realhref'/'realsrc' with the real value,
	 * which the browsers don't attempt to modify. This utility function gets
	 * the 'real' version of the given attribute if it's available.
	 * 
	 * @param Element node
	 * @param string attrName
	 * @return string
	 */
	BssEditor.prototype.getRealAttribute = function (node, attrName)
	{
		return (node.hasAttribute('real' + attrName) ? node.getAttribute('real' + attrName) : node.getAttribute(attrName));
	};
    
    BssEditor.prototype.createLinkOverlay = function ()
    {
        var self = this;
		var activeLink = this.getSelectionAncestor('a');
		var linkTitle = (activeLink ? activeLink.innerHTML : this.getSelectedHtml());
		var linkHref = (activeLink ? this.getRealAttribute(activeLink, 'href') : '');
        var linkOverlay = new BssOverlay({ width: '20em' });
        
        linkOverlay.setTitle((linkOverlay ? 'Edit link' : 'Add link'));
        
        linkOverlay.setMessage(
            '<div class="data-entry">' +
            '<div class="field"><label class="field-label field-linked" for="__link-title">Text to link: </label><input type="text" id="__link-title" class="full-length" value="' + linkTitle + '"></div>' +
            '<div class="field"><label class="field-label field-linked" for="__link-href">Address: </label><input type="text" id="__link-href" class="full-length" value="' + linkHref + '"></div>' +
            '</div>'
        );
        
        linkOverlay.addButton('ok', (activeLink ? 'Update' : 'Add link'), 'submit', function () {
            var href = $.trim($('#__link-href').val());
            var text = $.trim($('#__link-title').val()) || href;
            
            if (!href)
            {
                $('#__link-href').focus();
                
                if (!this.validationError)
                {
                    $('#__link-href').after('<div class="error">You must provide a valid URL/address.</div>');
                    this.validationError = true;
                }
                
                return;
            }
			else if (!href.match(/^(https?|ftp):\/\//i))
			{
				
			}
			
			var existingLink = self.getSelectionAncestor('a');
			
			if (existingLink)
			{
				self.selectElement(existingLink);
			}
            
            self.insertHtml('<a realhref="' + href + '" href="' + href + '">' + text + '</a>');
            self.getWindow().focus();
            this.close();
        });

		if (activeLink)
		{
			linkOverlay.addButton('delete', 'Remove', 'submit', function () {
				var link = self.getSelectionAncestor('a');
				
				self.selectElement(link);
				self.insertHtml(link.innerHTML);
				self.getWindow().focus();
				this.close();
			});
		}
        
        linkOverlay.addButton('cancel', 'Cancel', 'link', function () { this.close(); });
        linkOverlay.show();
        $('#__link-title').focus();
    };

	BssEditor.prototype.createImageOverlay = function ()
	{
        var self = this;
        var overlay = new BssOverlay({ width: '20em' });
        
		overlay.setTitle('Insert image');
        
        linkOverlay.setMessage(
            '<div class="data-entry">' +
			'<div class="field"><label class="field-label field-linked" for="__image-src">External image location (URL): </label><input type="text" id="__image-src" class="full-length" value="' + imageHref + '"></div>' +
			'<div class="field"><label class="field-label field-linked" for="__image-alt">Alternate text (optional): </label><input type="text" id="__image-alt" class="full-length" value="' + imageAlt + '"></div>' +
            '</div>'
        );
        
        linkOverlay.addButton('ok', (activeLink ? 'Update' : 'Add link'), 'submit', function () {
            var imageSrc = $.trim($('#__image-src').val());
            var imageAlt = $.trim($('#__image-alt').val()) || href;
            
            if (!href)
            {
                $('#__image-src').focus();
                
                if (!this.validationError)
                {
                    $('#__image-src').after('<div class="error">You must provide a valid URL/address.</div>');
                    this.validationError = true;
                }
                
                return;
            }
			else if (!imageSrc.match(/^(https?|ftp):\/\//i))
			{
				
			}
			
			var existingImage = self.getSelectionAncestor('img');
			
			if (existingImage)
			{
				self.selectElement(existingImage);
			}
            
			var imageTag = '<img realsrc="' + imageSrc + '" src="' + imageSrc + '" alt="' + imageAlt + '">';
            self.insertHtml(imageTag);
            self.getWindow().focus();
            this.close();
        });

		if (activeLink)
		{
			linkOverlay.addButton('delete', 'Remove', 'submit', function () {
				var image = self.getSelectionAncestor('img');
				
				self.selectElement(image);
				self.insertHtml('');
				self.getWindow().focus();
				this.close();
			});
		}
        
        overlay.addButton('cancel', 'Cancel', 'link', function () { this.close(); });
        overlay.show();
        $('#__image-src').focus();
	};
    
    BssEditor.prototype.getContent = function ()
    {
        return this.getDocument().body.innerHTML;
    };
    
    BssEditor.prototype.getSelection = function ()
    {
        var win = this.getWindow();
        return (win.getSelection ? win.getSelection() : win.document.selection);
    };
    
    BssEditor.prototype.getRange = function ()
    {
        var sel = this.getSelection();
        return (sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange());
    };

	BssEditor.prototype.selectElement = function (node)
	{
		if (this.getDocument().body.createControlRange)
		{
			var range;
			
			try
			{
				range = this.getDocument().body.createControlRange();
				range.addElement(node);
			}
			catch (ex)
			{
				range = this.getDocument().body.createTextRange();
				range.moveToElementText(node);
			}
			
			range.select();
		}
		else
		{
			var selection = this.getSelection();
			selection.removeAllRanges();
			
			var range = this.getDocument().createRange();
			range.selectNode(node);
			selection.addRange(range);
		}
	};
    
    BssEditor.prototype.getSelectedHtml = function ()
    {
        var range = this.getRange();
        return (range.toString ? range.toString() : range.htmlText);
    };

	BssEditor.prototype.isControlSelection = function (selection)
	{
		selection = selection || this.getSelection();
		
		if (selection.type)
		{
			// IE has the `type` property we can use directly.
			return (selection.type.toLowerCase() == 'control');
		}
		else if (selection.rangeCount == 1)
		{
			var range = selection.getRangeAt(0);
			return (range.startContainer == range.endContainer && range.endOffset == range.startOffset+1 && range.startContainer.nodeType != 3);
		}
		
		return false;
	};

	BssEditor.prototype.getSelectedNode = function ()
	{
		var range = this.getRange();
		var node = null;
		
		if (range.startContainer)
		{
			node = range.startContainer;
			
			if (node.nodeType != 3) // Not a text node.
			{
				node = node.childNodes[node.startOffset];
			}
		}
		else
		{
			node = (range.length > 0 ? range.item(0) : range.parentElement());
		}
		
		return node;
	};

	BssEditor.prototype.sanitizeHtml = function (html)
	{
		// Replace <em> with <i>, <strong> with <b>
		return html
			.replace(/<em(>|\s[^>]*>)/ig, '<i>')
			.replace(/<\/em>/ig, '</i>')
			.replace(/<strong(>|\s[^>]*>)/ig, '<b>')
			.replace(/<\/strong>/ig, '</b>');
	};
    
    BssEditor.prototype.insertHtml = function (html)
    {
        var win = this.getWindow();
        win.focus();
        
        if (this.restoreRange)
        {
            this.restoreRange.select();
        }
        
        win.focus();
        var range = this.getRange();

		html = this.sanitizeHtml(html);
        
        if (range.pasteHTML)
        {
            // IE does this on the range object.
            range.pasteHTML(html);
        }
        else
        {
            // Everything else uses execCommand().
            this.getDocument().execCommand('inserthtml', false, html);
            win.focus();
        }
    };
    
    BssEditor.prototype.setWysiwyg = function (on)
    {
        if (this.wysiwyg != on)
        {
            this.wysiwyg = on;
            var doc = this.getDocument();
            var iframe = $('#'+this.wyid);
            var toolbar = $('#'+this.wyid+'-toolbar');
            var ta = $('#'+this.id);
            
            if (on)
            {
                iframe.show();
                this.configureDesignMode();
                doc.body.innerHTML = this.sanitizeHtml(ta.val()) || '';
                ta.hide();
            }
            else
            {
                ta.val(doc.body.innerHTML);
                iframe.hide();
                ta.show();
            }
        }
    };

	BssEditor.prototype.addSubmitElement = function (form)
	{
		if (!document.getElementById(this.wyid))
		{
			// If we've removed the wysiwyg editor, don't bother doing this anymore.
			return;
		}
		
		var hiddenId = this.wyid + '-hidden-input';
		var hiddenInput = document.getElementById(hiddenId);
		var textArea = document.getElementById(this.id);
		
		if (!hiddenInput)
		{
			hiddenInput = document.createElement('INPUT');
			hiddenInput.type = 'hidden';
			hiddenInput.name = textArea.name;
			hiddenInput.id = hiddenId;
			
			// Remove the textarea's name so it doesn't get submitted.
			textArea.name = '';
			
			form.appendChild(hiddenInput);
		}
		
		// Update the value to match the textarea's.
		hiddenInput.value = (this.wysiwyg ? this.getDocument().body.innerHTML : this.sanitizeHtml($(textArea).val()));
	};
    
    BssEditor.prototype.addEventListener = function (eventName, handler)
    {
        if (!this.eventListeners[eventName])
        {
            this.eventListeners[eventName] = [handler];
        }
        else
        {
            this.eventListeners[eventName][this.eventListeners[eventName].length] = handler;
        }
    };
    
    BssEditor.prototype.fireEvent = function (eventName)
    {
        if (this.eventListeners[eventName])
        {
            var self = this;
            var i;
            
            for (i = 0; i < this.eventListeners[eventName].length; i++)
            {
                this.eventListeners[eventName][i].apply(self, arguments);
            }
        }
    };
    
    $.fn.wysiwyg = function (settings)
    {
        this.each(function () {
            new BssEditor(this.id, settings);
        });
    };
})(jQuery);
