/**************************************************
 * Codex, version 0.1
 * Copyright 2005 Sumul Shah
 * This free software is licensed under the GNU LGPL 
 * (http://creativecommons.org/licenses/LGPL/2.1/)
 **************************************************/

/*
This software is very experimental, and I appreciate any help 
in making it better. Please report bugs, fixes, suggestions, or
other comments to sumul@sumulshah.com.
*/

/* 
Things to keep in mind: 
	- <hr class="newpage"> will force a new page
	
	- images (and other non-text leaf nodes) get resized if they are too big
	
	- currently uses the "start" attribute to continue numbering an <ol> on a new page, 
		but "start" is deprecated. Code is written to use empty <li>'s to continue numbering. 
		Maybe make it an option.
		
	- if an <li> spans two pages, it gets the class "li-continued" on the new page.
		This is useful for hiding the bullet on the second page.
	
	- if an element with an id requires more than one page to display, 
		the element will have the pagenumber appended to its id on subsequent pages.
		e.g. if the entire book content is wrapped in a <div id="foo">, then the contents
		of page 1 will be wrapped in <div id="foo">, the contents of page 2 will be 
		wrapped in <div id="foo-2">, etc.
		
	- the following elements are available for styling: 
		div#book - main book container
		div.pageset - two pages
		div#pageset-n - the nth set of two pages
		div.bookpage - a page of the book. height is required
		div#bookpage-n - the nth page of the book
		li.li-continued - an <li> continued on a page from a previous page
		div#booknav - the container for page navigation elements
		a#booknav-prev - the previous page link
		a#booknav-next - the next page link
		div#booknav-current - the current page indicator
		span.booknav-pagenumber
			#booknav-page1 - the page number of the first page
			#booknav-page2 - the page number of the second page
		
	- IE only allows padding and border on .bookpage to be assigned in px
*/

/******************************
 * Make the book
 ******************************/

// define some node types
var TEXT_NODE = 3;
var COMMENT_NODE = 8;

// keep track of the number of pages made
var pagecount = 0;

// keep track of the number of pagesets
var setcount = 0;

// keep track of the current pageset being displayed
var currentset = 1;

// make it
function makeBook() {	
	// read the book's content source
	var source = document.getElementById("bookcontent");
	if(!source) {
		return;
	}
	
	// save its child nodes
	var source_nodes = source.childNodes;
	
	// make the book container
	var book = document.createElement("div");	// create the book
	book.id = "book";							// give it the book id
		
	// put the book in the document, where the content source currently is
	source.parentNode.insertBefore(book, source);
	
	// make the first page
	var page = makePage();
	
	// keep track of the heirarchy in an array
	var parents = new Array();
	
	// keep track of the number of <li>'s for numbering <ol>'s that break across pages
	var ol_li_count = 0;
		
	// put all of the childnodes in pages (recurse through children of children)
	for(var i=0; i<source_nodes.length; i++) {
		placeNodes(source_nodes[i], page);
	}
		
	// recursive function to place all childnodes in pages
	// returns the parent of the node that was placed
	function placeNodes(node, parent) {
		if(node.hasChildNodes()) {		
			// if this node is an <ol>, reset the number of <li>'s 
			if(node.nodeName == "OL") {
				ol_li_count = 0;
			}
			
			// if this node is an <li>, keep track for <ol> numbering purposes
			if(node.nodeName == "LI") {
				ol_li_count++;
			}
						
			// place an empty clone of the node
			var node_clone = parent.appendChild(node.cloneNode(false));
			
			// if the empty clone causes the page to overflow (like the bullet of an empty <li>),
			// delete the node, make a new page, add the clone to the new page
			if(overflow(page)) {
				// delete the cloned node
				parent.removeChild(node_clone);
				
				// make a new page
				page = makePage();
							
				// recreate the heirarchy on the new page
				var clone_parent = makeHeirarchy(page, parents, ol_li_count);
				
				// make the clone again on the new page
				node_clone = clone_parent.appendChild(node.cloneNode(false));
			}
			
			// keep track of the heirarchy
			parents.push(node);
			
			// recurse through the children
			for(var j=0; j<node.childNodes.length; j++) {			
				node_clone = placeNodes(node.childNodes[j], node_clone);
				
				// nodeClode gets updated above based on whether the placement of a child node
				// causes a new page to be made (and thus a new cloned node to place nodes in)
			}
			
			// keep track of the heirarchy
			parents.pop(node);
			
			return node_clone.parentNode;
		}
		
		// if there are no children, this is a text node (or image, etc.)
		else {
			// keep track of the parent node of the placed clone node
			var clone_parent = parent;
		
			// if this is a text node
			if(node.nodeType == TEXT_NODE) {						
				// add the text node to the page
				var text_node_clone = addNode(parent, node);
				
				// if the text node made the page overflow
				if(overflow(page)) {							
					// clear the text node that was just made
					text_node_clone.nodeValue = "";
					
					// add words until it's too long
					var text = node.nodeValue;	// the text that wouldn't fit
					var next_word = "";			// the next word to be added			
					
					// keep adding words and making new pages (if necessary) until there is no more text
					while(isText(text)) {							
						while(!overflow(page) && isText(text)) {
							// get the next word from the text
							next_word = text.match(/\s*\S+\s*/);
							next_word = next_word.toString();	
							
							// add it to the page
							text_node_clone.nodeValue = text_node_clone.nodeValue + next_word;
														
							// delete it from the text 
							text = text.substr(text.indexOf(next_word)+next_word.length);	
						}			
						
						// if the last word caused the page to overflow, 
						// delete it from the page, and add it back to the text
						if(overflow(page)) {						
							// delete it from the page
							text_node_clone.nodeValue = text_node_clone.nodeValue.substring(0, text_node_clone.nodeValue.lastIndexOf(next_word));
							
							// add it back to the text
							text = next_word + text;
							
							// if the page now contains an empty node
							var empty_node = null;
							if(text_node_clone.nodeValue == "") {								
								// delete the last node and save it for later
								empty_node = parent.parentNode.removeChild(parent);
								
								// pop the node off the parents array
								parents.pop();
							}
						}						
						
						// if there is still text left, make a new page
						if(isText(text)) {										
							// make a new page
							page = makePage();
							
							// recreate the heirarchy on the new page and add the blank text node
							clone_parent = makeHeirarchy(page, parents, ol_li_count);
							
							// if the last page ended in an empty node, add it to the heirarchy
							if(empty_node != null) {
								// add it to the end of the heirarchy
								clone_parent = addNode(clone_parent, empty_node);
								
								// push it onto the parents array
								parents.push(empty_node);
							}
							
							text_node_clone = addNode(clone_parent, node);	// add the text node to the heirarhcy
							text_node_clone.nodeValue = "";				// clear the text node				
						} 
					} // endwhile
				} // end if (page overflow)
			} // end if(text node)
			
			// if this is not a text node 
			else {				
				// if the node is <hr class="newpage">
				if(node.nodeName == "HR" && node.className == "newpage") {
					// make a new page
					page = makePage();
					
					// recreate the heirarchy on the new page
					clone_parent = makeHeirarchy(page, parents, ol_li_count);
				}
							
				// add the node to the page
				var leaf_node_clone = addNode(clone_parent, node);
				
				// if the node made the page overflow
				if(overflow(page)) {
					// remove the node
					parent.removeChild(leaf_node_clone);
								
					// make a new page
					page = makePage();
					
					// recreate the heirarchy on the new page and add the node
					clone_parent = makeHeirarchy(page, parents, ol_li_count);	
					leaf_node_clone = addNode(clone_parent, node);	// add the node to the heirarhcy
					
					// if the node was too tall for the new page, shrink it
					if(overflow(page)) {
						var aspect_ratio = leaf_node_clone.width/leaf_node_clone.height;
						
						leaf_node_clone.height = pageHeight(page)
												- style2num(CJL_getCurrentStyle(leaf_node_clone, "margin-top"))
												- style2num(CJL_getCurrentStyle(leaf_node_clone, "margin-bottom"))
												- style2num(CJL_getCurrentStyle(leaf_node_clone, "padding-top"))
												- style2num(CJL_getCurrentStyle(leaf_node_clone, "padding-bottom"))
												- style2num(CJL_getCurrentStyle(leaf_node_clone, "border-top"))
												- style2num(CJL_getCurrentStyle(leaf_node_clone, "border-bottom"));
												
						leaf_node_clone.width = leaf_node_clone.height*aspect_ratio;
					}
				}
			}
					
			// if a new page was made, notify the calling function that the parent node has changed
			// otherwise, return the original parent
			return clone_parent;
				
		} // endelse (no children)
	} // end function 
		
	// remove the book source from the document
	source.parentNode.removeChild(source);

	// hide all but the first pages
	for(var k=2; k<=setcount; k++) {
		document.getElementById("pageset-"+k).style.display = "none";
	}
	
	// make the navigation 
	makeNav();
		
	return;
}

/******************************
 * Functions for making pages
 ******************************/

// makes a page of the book 
// returns the page element
function makePage() {
	// if this is an even numbered page (0, 2, 4, ...), make a new pageset
	if(pagecount % 2 == 0) {
		pageset = makePageSet();
	}

	var page = document.createElement("div");	// create the page
	page.className = "bookpage";				// give it the bookpage class
	page.id = "bookpage-" + (pagecount + 1);	// set the id based on the number
	page.style.overflow = "hidden";				// hide overflow so IE height stays defined
	
	// put the page in the current set
	pageset.appendChild(page);
	
	// increment the page counter
	pagecount++;
	
	return page;
}

// makes a pageset of the book, which contains two pages
// returns the pageset element
function makePageSet() {
	var set = document.createElement("div");	// create the set
	set.className = "pageset";					// give it the pageset class
	set.id = "pageset-" + ((pagecount/2) + 1);	// set the id based on the number
	
	// put the set in the book
	var book = document.getElementById("book");
	book.appendChild(set);
	
	// increment the set counter
	setcount++;
	
	return set;	
}

// recreates the heirarchy of the last filled page on a newly created page
// returns the last node in the heirarchy
function makeHeirarchy(page, parents, ol_li_count) {
	var clone_parent = page;
	for(var z=0; z<parents.length; z++) {
		clone_parent =  clone_parent.appendChild(parents[z].cloneNode(false));
		
		// if a node with an ID was just placed, append the pagenumber to the ID 
		// to avoid having multiple nodes with the same ID in the DOM tree
		if(clone_parent.id != "") {
			clone_parent.id += "-" + pagecount;
		}
		
		// if an <ol> was just placed set the start attribute such that numbering will continue where it left off
		if(clone_parent.nodeName == "OL") {
			clone_parent.setAttribute("start", ol_li_count);
		}
		
		/*
		// if an <ol> was just placed, add enough <li>'s so that numbering will resume correctly
		if(clone_parent.nodeName == "OL") {
			for(var k=0; k<ol_li_count-1; k++) {
				var li_pad = document.createElement("li");
				li_pad.style.visibility = "hidden";
				li_pad.style.height = "0";
				clone_parent.appendChild(li_pad);
			}
		}
		*/
		
		// if an <li> was just placed, give it the class "li-continued" so that it can be styled
		// as a continuation from the previous page
		if(clone_parent.nodeName == "LI") {
			clone_parent.className = "li-continued " + clone_parent.className;
		}
	}
	
	return clone_parent;
}

// Add a node to a parent
// Does not add empty text nodes	
function addNode(parent, node) {
	// if it's not a text node, add it
	if(node.nodeType != TEXT_NODE) {
		return parent.appendChild(node.cloneNode(true));
	}
	// if it is a text node that has some content, add it
	else if(node.nodeValue.search(/\S/i) != -1) {
		return parent.appendChild(node.cloneNode(true));
	}
}

/******************************
 * Navigating pages
 ******************************/
 
// make the navigation 
function makeNav() {
	// make the navigation container
	var container = document.createElement("div");
	container.id = "booknav";	// give it the id "booknav"
	
	// make the previous link
	var prev = document.createElement("a");
	prev.id = "booknav-prev";
	prev.setAttribute("href", "javascript: prevPage();");
//	prev.onclick = function() { prevPage(); }
	prev.appendChild(document.createTextNode("Previous"));
	
	// make the next link
	var next = document.createElement("a");
	next.id = "booknav-next";
	next.setAttribute("href", "javascript: nextPage();");
//	next.onclick = function() { nextPage(); }
	next.appendChild(document.createTextNode("Next"));
	
	// make the current pageset indicator
	var indicator = document.createElement("div");
	indicator.id = "booknav-current";
	indicator.appendChild(document.createTextNode(currentset + " of " + setcount));

	// make the first page number
	var page1 = document.createElement("span");
	page1.className = "booknav-pagenumber";
	page1.id = "booknav-page1";
	page1.appendChild(document.createTextNode("Page"));
	
	// make the second page number
	var page2 = document.createElement("span");
	page2.className = "booknav-pagenumber";
	page2.id = "booknav-page2";
	page2.appendChild(document.createTextNode("Page"));
		
	/*
	// make the page numbers container
	var pagenums = document.createElement("div");
	pagenums.id = "booknav-pagenumbers";

	// add the page numbers to the page numbers container
	pagenums.appendChild(page1);
	pagenums.appendChild(document.createTextNode(" "));
	pagenums.appendChild(page2);
	*/
	
	// add the components to the container
	container.appendChild(indicator);	// add indicator
	container.appendChild(document.createTextNode(" "));
	container.appendChild(page1);		// add page 1
	container.appendChild(document.createTextNode(" "));
	container.appendChild(page2);		// add page 2
	container.appendChild(document.createTextNode(" "));
	container.appendChild(prev);		// add previous link 
	container.appendChild(document.createTextNode(" "));
	container.appendChild(next);		// add next link 

	/* 
	// add the container to the document (before the book)
	var book = document.getElementById("book");
	book.parentNode.insertBefore(container, book);
	*/
	
	// add the container to the end of the book
	document.getElementById("book").appendChild(container);
	
	// update the page navigation
	updateNav();
}

// show the next page
function nextPage() {
	if(currentset < setcount) {
		// get the current and next pagesets
		var current = document.getElementById("pageset-"+currentset);
		var next = document.getElementById("pageset-"+(currentset+1));
		
		// show the next, hide the current
		next.style.display = current.style.display;
		current.style.display = "none";
		
		// update the current set
		currentset++;
		
		// update the page navigation
		updateNav();
	}
}

// show the previous page
function prevPage() {	
	if(currentset > 1) {
		// get the current and previous pagesets
		var current = document.getElementById("pageset-"+currentset);
		var prev = document.getElementById("pageset-"+(currentset-1));
		
		// show the next, hide the current
		prev.style.display = current.style.display;
		current.style.display = "none";
		
		// update the current set
		currentset--;
		
		// update the page navigation
		updateNav();
	}
}

// if there are no "next" or "previous" pages, hide the link 
// update the current page indicator
function updateNav() {
	// if there are no more next pages
	if(currentset == setcount) {
		// hide the next link
		document.getElementById("booknav-next").style.visibility = "hidden";
	}
	else {
		var next = document.getElementById("booknav-next");
		next.style.visibility = "visible";
//		next.firstChild.nodeValue = "Next ("+(setcount - currentset) +" more)";
	}
	
	// if there are no more previous pages
	if(currentset == 1) {
		// hide the previous link
		document.getElementById("booknav-prev").style.visibility = "hidden";;
	}
	else {
		var prev = document.getElementById("booknav-prev");
		prev.style.visibility = "visible";
//		prev.firstChild.nodeValue = "Previous ("+(currentset-1) +" more)";
	}
	
	// update the current page indicator
	updateIndicator();
	
	// update the page numbers
	updatePageNums();
}

// update the current page indicator
function updateIndicator() {
	var indicator = document.getElementById("booknav-current");
	indicator.firstChild.nodeValue = (currentset + " of " + setcount);
}

// update the page numbers
function updatePageNums() {
	// get page numbers spans
	var page1 = document.getElementById("booknav-page1");
	var page2 = document.getElementById("booknav-page2");
	
	// calculate page numbers
	var num1 = "Page " + (((currentset-1)*2)+1) + " of " + pagecount;
	var num2 = ((currentset-1)*2)+2;
	
	// if there is only content on the first page of the pageset
	if(num2 > pagecount) {
		num2 = "";
	}
	else {
		num2 = "Page " +  num2 + " of " + pagecount;
	}

	// put page numbers in spans	
	page1.firstChild.nodeValue = num1;
	page2.firstChild.nodeValue = num2;
}

/******************************
 * Supplemental functions
 ******************************/
 
// IE 5.0 does not support the push or pop methods for arrays
// implement them using code from http://www.dithered.com/javascript/array/index.html
var undefined;
function isUndefined(property) {
  return (typeof property == 'undefined');
}
// Array.push() - Add an element to the end of an array
if (isUndefined(Array.prototype.push) == true) {
  Array.prototype.push = function() {
     var currentLength = this.length;
     for (var i = 0; i < arguments.length; i++) {
        this[currentLength + i] = arguments[i];
     }
     return this.length;
  };
}
// Array.pop() - Remove the last element of an array and return it
if (isUndefined(Array.prototype.pop) == true) {
  Array.prototype.pop = function() {
     var lastItem = undefined;
    if ( this.length > 0 ) {
        lastItem = this[this.length - 1];
        this.length--;
    }
    return lastItem;
  };
}

// returns true if there is non-whitespace content in text, false otherwise
function isText(text) {
	return text.length && (text.search(/\S/) != -1);
}

// return true if the page has overflown, false otherwise
function overflow(page) {
	return childBottom(page) > pageBottom(page);
}

// returns the distance from the top of the body to the bottom of the last child
function childBottom(page) {
	if(!page.lastChild)
		return 0;
	else
		var child = page.lastChild;
		
	if(child.offsetParent == document.body) {
		return child.offsetHeight+child.offsetTop;
	}
	else {
		return child.offsetHeight+child.offsetTop+getTotalOffset(child.offsetParent);
	}
}

// returns the distance from the top of the body to the bottom of the content of the page
function pageBottom(page) {
	var paddingB = 0;	// bottom padding of the page
	var borderB = 0;	// bottom border of the page
	
	/*
	if (page.currentStyle) {	// get the computed style from IE
		paddingB = page.currentStyle["paddingBottom"];
		borderB = page.currentStyle["borderBottomWidth"];
	} 
	else if (document.defaultView.getComputedStyle) {	// get the computed style from other browsers
		var compStyle = document.defaultView.getComputedStyle(page, '');
		paddingB = compStyle.getPropertyValue("padding-bottom");
		borderB = compStyle.getPropertyValue("border-bottom-width");
	}
	*/
	
	paddingB = CJL_getCurrentStyle(page, "padding-bottom");
	borderB = CJL_getCurrentStyle(page, "border-bottom");
	
	// strip the "px" from the padding and border values
	paddingB = style2num(paddingB);	
	borderB = style2num(borderB);
		
	if(page.offsetParent == document.body)
		return page.offsetHeight+page.offsetTop-paddingB-borderB;
	else
		return page.offsetHeight+page.offsetTop+getTotalOffset(page.offsetParent)-paddingB-borderB;
}

// returns the offset from the top of the screen to the top of element
// no matter how many offsetParents are in between
function getTotalOffset(element) {
	if(element == undefined) {
		return 0;
	}
	if(element.offsetParent == null) {
		return 0;
	}
	
	if(element.offsetParent == document.body) {
		return element.offsetTop;
	}
	else {
		return element.offsetTop+getTotalOffset(element.offsetParent);
	}
}

// returns the height of the page
function pageHeight(page) {
	var paddingB = 0;	// bottom padding of the page
	var borderB = 0;	// bottom border of the page
	var paddingT = 0;	// top padding of the page
	var borderT = 0;	// top border of the page
	
	/*
	if (page.currentStyle) {	// get the computed style from IE
		paddingB = page.currentStyle["paddingBottom"];
		borderB = page.currentStyle["borderBottomWidth"];
		paddingT = page.currentStyle["paddingTop"];
		borderT = page.currentStyle["borderTopWidth"];
	} 
	else if (document.defaultView.getComputedStyle) {	// get the computed style from other browsers
		var compStyle = document.defaultView.getComputedStyle(page, '');
		paddingB = compStyle.getPropertyValue("padding-bottom");
		borderB = compStyle.getPropertyValue("border-bottom-width");
		paddingT = compStyle.getPropertyValue("padding-top");
		borderT = compStyle.getPropertyValue("border-top-width");
	}
	*/
	
	paddingB = CJL_getCurrentStyle(page, "padding-bottom");
	borderB = CJL_getCurrentStyle(page, "border-bottom");
	paddingT = CJL_getCurrentStyle(page, "padding-top");
	borderT = CJL_getCurrentStyle(page, "border-top");
	
	// strip the "px" from the padding and border values
	paddingB = style2num(paddingB);
	borderB = style2num(borderB);
	paddingT = style2num(paddingT);
	borderT = style2num(borderT);
	
	return page.offsetHeight-paddingB-borderB-paddingT-borderT;
}

// Cross-platform JavaScript method for retrieving style information.
// From http://www.codehouse.com/javascript/scripts/cjl/get_current_style/
function CJL_getCurrentStyle(elem, prop) {
   if( elem.currentStyle )
   {  
      var ar = prop.match(/\w[^-]*/g);
      var s = ar[0];
      
      for(var i = 1; i < ar.length; ++i)		   
      {
         s += ar[i].replace(/\w/, ar[i].charAt(0).toUpperCase());
      }
      return elem.currentStyle[s]
   }
   else if( document.defaultView.getComputedStyle )
   {
      return document.defaultView.getComputedStyle(elem, null).getPropertyValue(prop);
   }
}

// converts a numeric style value (e.g. "15px") to a number (e.g. 15)
function style2num(value) {
	if(value)
		return (+value.substring(0,value.search(/\D/)));
	else
		return 0;
}
	
// make the book when the page loads
function addLoadHandler(handler)
{
	if(window.addEventListener)
	{
		window.addEventListener("load",handler,false);
	}
	else if(window.attachEvent)
	{
		window.attachEvent("onload",handler);
	}
	else if(window.onload)
	{
		var oldHandler = window.onload;
		window.onload = function piggyback()
		{
			oldHandler();
			handler();
		};
	}
	else
	{
		window.onload = handler;
	}
}
if(document.getElementById && document.createElement)
	addLoadHandler(function(){ makeBook(); });
