// wiki.js - JavaScript implementation of a modeless wiki WYSIWYG editor
// Copyright (C) 2007 Michael Leonhard
// http://SeriousIT.com/

// Check for fvlogger, http://alistapart.com/articles/jslogging
try {
    var test = debug;
    test = info;
    test = warn;
    test = error;
} catch (e) {
    // fvlogger is not loaded, use dummy functions
    debug = info = warn = error = function (S) {};
}
debug("Loading wiki.js");

// Thanks to Jan` from ##javascript@freenode for this:
Function.prototype.bind = function(object)
{
    var __method = this;
    var boundFun = function() { __method.apply(object, arguments); };
    alert("Bound fun to object " + object.toString() + ":\n" + boundFun.toString());
    return boundFun;
};

// typeOf - returns the type of the value: 'object', 'array', 'number', or 'function'
// written by Douglas Crockford
// http://javascript.crockford.com/remedial.html
function typeOf(value) {
    var s = typeof value;
    if (s === 'object') {
        if (value) {
            if (typeof value.length === 'number' &&
                !(value.propertyIsEnumerable('length')) &&
                typeof value.splice === 'function') {              
                s = 'array';
            }
        } else {
            s = 'null';
        }
    }
    return s;
}


// isEmpty - isEmpty(v) returns true if v is an object containing no enumerable members
// written by Douglas Crockford
// http://javascript.crockford.com/remedial.html
function isEmpty(o) {
    var i, v;
    if (typeOf(o) === 'object') {
        for (i in o) {
            v = o[i];
            if (v !== undefined && typeOf(v) !== 'function') {
                return false;
            }
        }
    }
    return true;
}

// entityify - produces a string in which '<', '>', and '&' are replaced with their HTML entity equivalents
// written by Douglas Crockford
// http://javascript.crockford.com/remedial.html
String.prototype.entityify = function () {
    return this.replace(/&/g, "&amp;").replace(/</g,
					       "&lt;").replace(/>/g, "&gt;");
};

// quote - returns the supplied string surrounded by quotes, with all quotes and backslashes escaped
// written by Douglas Crockford
// http://javascript.crockford.com/remedial.html
String.prototype.quote = function () {
    var c, i, l = this.length, o = '"';
    for (i = 0; i < l; i += 1) {
        c = this.charAt(i);
        if (c >= ' ') {
            if (c === '\\' || c === '"') {
                o += '\\';
            }
            o += c;
        } else {
            switch (c) {
            case '\b':
                o += '\\b';
                break;
            case '\f':
                o += '\\f';
                break;
            case '\n':
                o += '\\n';
                break;
            case '\r':
                o += '\\r';
                break;
            case '\t':
                o += '\\t';
                break;
            default:
                c = c.charCodeAt();
                o += '\\u00' + Math.floor(c / 16).toString(16) +
                    (c % 16).toString(16);
            }
        }
    }
    return o + '"';
};

// supplant - does variable substition
// "abc{FOO}123".supplant({'FOO':'BAR'}) returns "abcBAR123"
// written by Douglas Crockford
// http://javascript.crockford.com/remedial.html
String.prototype.supplant = function (o) {
    var i, j, s = this, v;
    for (;;) {
        i = s.lastIndexOf('{');
        if (i < 0) {
            break;
        }
        j = s.indexOf('}', i);
        if (i + 1 >= j) {
            break;
        }
        v = o[s.substring(i + 1, j)];
        if (typeOf(v) !== 'string' && typeOf(v) !== 'number') {
            break;
        }
        s = s.substring(0, i) + v + s.substring(j + 1);
    }
    return s;
};

// trim - removes whitespace characters from the beginning and end of the string
// written by Douglas Crockford
// http://javascript.crockford.com/remedial.html
String.prototype.trim = function () {
    return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1");
}; 

// purge - remove links to DOM, working around IE memory leak
//
// The purge function takes a reference to a DOM element as
// an argument. It loops through the element's attributes.
// If it finds any functions, it nulls them out. This breaks
// the cycle, allowing memory to be reclaimed. It will also look
// at all of the element's descendent elements, and clear out
// all of their cycles as well. The purge function is harmless
// on Mozilla and Opera. It is essential on IE. The purge
// function should be called before removing any element, either
// by the removeChild method, or by setting the innerHTML
// property.
//
// written by Douglas Crockford
// http://javascript.crockford.com/memory/leak.html
function purge(d) {
    var a = d.attributes, i, l, n;
    if (a) {
        l = a.length;
        for (i = 0; i < l; i += 1) {
            n = a[i].name;
            if (typeof d[n] === 'function') {
                d[n] = null;
            }
        }
    }
    a = d.childNodes;
    if (a) {
        l = a.length;
        for (i = 0; i < l; i += 1) {
            purge(d.childNodes[i]);
        }
    }
}


function assert(test)
{
    if (typeof(test) !== "boolean")
    {
	throw new Error("assert failed: expected boolean but got"+typeof(test));
    }
    else if (test === false)
    {
	throw new Error("assert failed");
    }
};
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////
// Color class
//////////////////////////////////////////////////////////////////////

// constructor: string -> object
// @param colorString a color value as defined in CSS3.  This can be a
//  name, #rgb, #rrggbb, rgb(rrr,ggg,bbb)
// @returns object derived from the string
// BUGS: color names, rgb(rrr%,ggg%,bbb%), and RGBA are unsupported.
//       See http://www.w3.org/TR/css3-color/#colorunits
// Examples:
// new Color("") --> Color #808080 r=128, g=128, b=128
// new Color("#abcdef") --> Color{r=171, g=205, b=239}
// new Color("#010203") --> Color{r=1, g=2, b=3}
// new Color("#abc") --> Color #AABBCC
// new Color("rgb(10, 20, 30 )") --> Color{r=10, g=20, b=30}
// new Color("rgb(100%, 50%, 25%)") --> Color #FF8040
// new Color() --> exception
// new Color(1) --> exception
function Color(colorString) {
    if (typeOf(colorString) === 'string') {} 
    else { throw new Error("Color object constructor accepts only strings"); }
    this.init(colorString);
    // implicit return this
}

// init: string -> none
// Sets default rgb values.  Extracts rgb values from string.
Color.prototype.init = function (colorString) {
    colorString = colorString.toLowerCase().trim();

    // #rgb
    var re = RegExp("^#([a-f0-9])([a-f0-9])([a-f0-9])$");
    var rgb = re.exec(colorString);
    if (typeOf(rgb) === 'array') {
	this.r = parseInt(rgb[1] + rgb[1], 16);
	this.g = parseInt(rgb[2] + rgb[2], 16);
	this.b = parseInt(rgb[3] + rgb[3], 16);
	debug("Color::init('" + colorString + "') Processed r=" + this.r + " g=" + this.g + " b=" + this.b);
	return;
    }
    
    // #rrggbb
    re = RegExp("^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$");
    rgb = re.exec(colorString);
    if (typeOf(rgb) === 'array') {
	this.r = parseInt(rgb[1], 16);
	this.g = parseInt(rgb[2], 16);
	this.b = parseInt(rgb[3], 16);
	debug("Color::init('" + colorString + "') Processed r=" + this.r + " g=" + this.g + " b=" + this.b);
	return;
    }
    
    // rgb(rrr, ggg, bbb)
    re = new RegExp("^rgb[(]\\s*([0-9]{1,3})\\s*,\\s*([0-9]{1,3})\\s*,\\s*([0-9]{1,3})\\s*[)]$");
    rgb = re.exec(colorString);
    if (typeOf(rgb) === 'array') {
	this.r = parseInt(rgb[1], 10) & 0xFF; // bits are chopped, leaving value 0-255
	this.g = parseInt(rgb[2], 10) & 0xFF;
	this.b = parseInt(rgb[3], 10) & 0xFF;
	debug("Color::init('" + colorString + "') Processed r=" + this.r + " g=" + this.g + " b=" + this.b + " " + this);
	return;
    }
    debug("Color::init('" + colorString + "') Skipped rgb(rrr,ggg,bbb) on \"" + colorString + "\"");
    
    // default
    this.r = 128;
    this.g = 128;
    this.b = 128;
}

// toString: none -> String
// Overrides the default object.toString() function
// @returns a string representation of the color, such as "#ABCDEF"
// @sideffects none
Color.prototype.toString = function () {
    var x = (this.r << 16) + (this.g << 8) + this.b;
    colorString = x.toString(16).toUpperCase();
    while (colorString.length < 6) {
	colorString = "0" + colorString;
    }
    return "#" + colorString;
}

// ColorTest: none -> none
// Creates various Color objects and tests their properties
function ColorTest() {
    // TODO convert these to use an array of test cases, print good errors

    testCases = [
	// default cases
	["", '#808080'],
	["#", "#808080"],
	["#a", "#808080"],
	["#ab", "#808080"],
	["#abcd", "#808080"],
	["#abcde", "#808080"],
	["rgb(1,2,3", "#808080"],
	["rgb(12,3)", "#808080"],
	["rgb(1,2 3)", "#808080"],
	["rgb(1 2 3)", "#808080"],
	// #rgb
	["#abc", "#AABBCC"],
	["#000", "#000000"],
	["#123", "#112233"],
	// #rrggbb
	["#000000", "#000000"],
	["#112233", "#112233"],
	["#AABBCC", "#AABBCC"],
	["#AbCdEf", "#ABCDEF"],
	["#ffffff", "#FFFFFF"],
	// rgb(rrr,ggg,bbb)
	["rgb(255,128,64)", "#FF8040"],
	["rgb(1,2,3)", "#010203"],
	["rgb( 1 , 2 , 3 )", "#010203"]
    ];
    
    // test each case
    for (var i = 0; i < testCases.length; i++) {
	var input = testCases[i][0];
	var expectedResult = testCases[i][1];
	var result = String(new Color(input));
	if (result === expectedResult) {}
	else {
	    throw new Error("ColorTest failure for input=\"" + input + "\": expected \"" + expectedResult + "\" but got \"" + str + "\"");
	}
    }
	
    // test r, g, and b members
    var color = new Color("");
    if (color.r === 128) {} else { throw new Error("ColorTest failure"); }
    if (color.g === 128) {} else { throw new Error("ColorTest failure"); }
    if (color.b === 128) {} else { throw new Error("ColorTest failure"); }
    var color = new Color("#ABCDEF");
    if (color.r === 171) {} else { throw new Error("ColorTest failure"); }
    if (color.g === 205) {} else { throw new Error("ColorTest failure"); }
    if (color.b === 239) {} else { throw new Error("ColorTest failure"); }
    
    // test stability
    var color = new Color("#ABCDEF");
    if (color.toString() === '#ABCDEF') {} else { throw new Error("ColorTest failure"); }
    color = new Color(color.toString());
    if (color.toString() === '#ABCDEF') {} else { throw new Error("ColorTest failure"); }
    color = new Color(String(color));
    if (color.toString() === '#ABCDEF') {} else { throw new Error("ColorTest failure"); }
} // ColorTest

//////////////////////////////////////////////////////////////////////
// WikiBlock class
//////////////////////////////////////////////////////////////////////

// WikiBlock object constructor
// @param divID the ID of a div, with class="block", to be converted.
//   The div and children are searched for text.  The div is replaced
//   with a wikiblock div that contains the same text, but editable.
// @returns the new WikiBlock object
function WikiBlock(divID) {
    // srcDiv is the block that will be replaced with a wikiblock
    srcDiv = document.getElementById(divID);
    if (srcDiv === null) { throw new Error("Element not found"); }
    if (srcDiv.tagName.toLowerCase() === "div") {} else { throw new Error("assert failed"); }
    if (srcDiv.className === "block") {} else { throw new Error("assert failed"); }
    
    // create replacement
    this.div = document.createElement("div");
    this.div.className = "wikiblock";
    this.copyText(srcDiv);
    
    // add event handlers
    thisObj = this;
    //handleEventFun = function (mouseEvent) { thisObj.changeBackground(mouseEvent.target); }
    handleEventFun = function (mouseEvent) { thisObj.onClick(mouseEvent); }
    this.div.addEventListener("click", {"handleEvent":handleEventFun}, true);
    //this.div.onClick = handleEventFun;
    
    // create cursor
    this.cursorDiv = document.createElement("div");
    //this.cursorDiv.appendChild(document.createTextNode("|"));
    this.cursorDiv.className = "cursor";
    this.div.appendChild(this.cursorDiv);
    
    // start cursor blinking
    thisObj = this;
    timerFun = function () {
	window.setTimeout(timerFun, 500);
	var curVis = thisObj.cursorDiv.style["visibility"];
	thisObj.cursorDiv.style["visibility"] = (curVis === "hidden") ? "visible" : "hidden";
    }
    timerFun();
    
    // replace
    purge(srcDiv);
    srcDiv.parentNode.replaceChild(this.div, srcDiv);
    this.checkResize();
}

// WikiBlock::checkResize : none -> none
// Checkes if wikiblock width has changed, rebuilds display_line list
// @returns this
WikiBlock.prototype.checkResize = function () {
    if (this.div) {} else { throw new Error("assert failed"); }
    if (typeOf(this.div['offsetWidth']) === 'number') {} else { throw new Error("assert failed"); }
    
    var prevWidth = this['prevWidth'] || 0;
    if (prevWidth === this.div.offsetWidth) { return; }
    debug("WikiBlock::checkResize() width changed: " + prevWidth + " --> " + this.div.offsetWidth);
    this.prevWidth = this.div.offsetWidth;
    return this;
}

// WikiBlock::copyText : node -> none
// Recursive method to find text and add it to wikiblock
// @param node the node to search for text
// @returns null
WikiBlock.prototype.copyText = function (node) {
    if (this.div) {} else { throw new Error("assert failed"); }
    // each child
    for (var i = 0; i < node.childNodes.length; i++) {
	var child = node.childNodes[i];
	// import text and known nodes
	switch (child.nodeName.toLowerCase())
	{
	case "br":
	    var brNode = document.createElement("br");
	    this.div.appendChild(brNode);
	    break;
	case "#text":
	    for (var t = 0; t < child.nodeValue.length; t++) {
		letterDiv = document.createElement("div");
		//letterDiv.style['display'] = 'inline';
		letterDiv.className = 'letter';
		textNode = document.createTextNode(child.nodeValue[t]);
		letterDiv.appendChild(textNode);
		this.div.appendChild(letterDiv);
	    }
	    break;
	default:
	    // do nothing special for other
	    warn("WikiBlock::copyText(...) unknown node, " + child.nodeName.toLowerCase());
	}
	// recurse if node has children
	if (child.childNodes.length > 0) {
	    this.copyText(child);
	}
    }
}

WikiBlock.prototype.onClick = function (mouseEvent)
{
    if (this.div) {} else { throw new Error("assert failed"); }
    
    this.checkResize();
    
    var target = mouseEvent.target;
    var onRightSide = false;
    
    // mouse click on cursor
    if (target === this.cursorDiv) {
	target = this.cursorDiv.nextSibling || this.cursorDiv.previousSibling;
    }
    // mouse click on wikiblock div
    else if (target == this.div) {
	target = this.div.lastChild;
    }
    // mouse click on other object, possibly a child of div
    else {
	// clicked on right half of character
	// DOM2 doesn't have offset* attributes.  onRightSide will always be
	// false on browsers that don't support the offset* attributes.
	var left = target.offsetLeft || 0;
	var width = target.offsetWidth || 0;
	var right = left + width;
	var center = right - width/2;
	onRightSide = mouseEvent.clientX > center && mouseEvent.clientX < right;
    }
    
    // check if target is a child of this.div
    for (var i = 0; i < this.div.childNodes.length; i++)
    {
	if (this.div.childNodes[i] === target)
	{
	    break;
	}
    }
    
    // target is a child of div
    if (i < this.div.childNodes.length) {
	this.div.removeChild(this.cursorDiv);
	
	// right side of target
	if (onRightSide) {
	    // this works if target.nextSibling is null because
	    // insertBefore(obj,null) is equivalent to appendChild(obj)
	    this.div.insertBefore(this.cursorDiv, target.nextSibling);
	}
	else {
	    this.div.insertBefore(this.cursorDiv, target);
	}
	
	//this.changeBackground(target);
	mouseEvent.preventDefault();
	return;
    }
    // target is html object, so original target was not a child of div
    else if (target.tagName.toUpperCase() === "HTML") {
	warn("WikiBlock::onClick(...) click target not descendent of this.div");
    }
    // target is not a child of div, try target's parent
    else {
	this.onClick(target.parent);
    }
}

WikiBlock.prototype.changeBackground = function (node)
{
    colorObj = new Color(this.nextColor || "#AAAAFF");
    debug("WikiBlock::changeBackground(...) node.style.backgroundColor = " + node.style.backgroundColor + "\ncolorObj = " + String(colorObj));
    node.style["backgroundColor"] = String(colorObj);
    colorObj.r = (colorObj.r + 0x40) & 0xFF;
    colorObj.g = (colorObj.g + 0x20) & 0xFF;
    colorObj.b = (colorObj.b + 0x10) & 0xFF;
    this.nextColor = colorObj.toString();
}

function wikifyBlock (divID)
{
    return new WikiBlock(divID);
}

// Run module tests
//ColorTest();


// This file may be partially loaded.  To ensure a full load, this must
// be the last statement in the file.
var wiki_js_version = 0.1;
