//# Chalkboard - A Rich Javascript Workspace

/*
Copyright (c) 2009 Takashi Yamamiya

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

//## Initialization
// Global variables

var isEdit = false; // Because designMode property is not reliable
var isSource = false; // True if show source mode.
var wiki; // Wiki Object
var Platform; // Platform dependent behavior
var Auth = "dGlubGl6emllOmV2ZXJ5ZGF5"; // console.log(btoa("tinlizzie:everyday"));
var Body; // A body object of the editor.
window.evalCode = function(text) { return eval(text); };

// This function is called from	HTML file to set data location.

function initialize(wikiPath) {
  window.PlayArea = $("#playarea")[0];
  var editor = $("#editor")[0];

  // Initialize editor window
  editor.contentWindow.document.write(
    "<head><base target='_parent'/><style type='text/css'>" +
    "pre {" +
    "background-color: #ECF0F0;" +
    "font-family: 'Courier New', Courier, mono;" +
    "white-space: -moz-pre-wrap;" +
    "white-space: -pre-wrap;" +
    "white-space: -o-pre-wrap;" +
    "white-space: pre-wrap;" +
    "word-wrap: break-word;}" +
    "h1 { font-family: Verdana; border-bottom: 5px solid #8080c0 }" +

    "</style></head>");
  editor.contentWindow.document.close();

  $(".view").click(function(event){
		     event.preventDefault();
		     showSource(false);
		   });

  $(".source").click(function(event){
		     event.preventDefault();
		     showSource(true);
		   });

  $(".printIt").click(function(event){
			event.preventDefault();
			printIt(editor.contentWindow.document);
		      });

  $(".doIt").click(function(event){
		     event.preventDefault();
		     doIt(editor.contentWindow.document);
		   });

  $(".run").click(function(event){
		     event.preventDefault();
		     run();
		   });

  $(".save").click(function(event){
		     event.preventDefault();
		     save();
		   });

  $(".edit").click(function(event){
		     event.preventDefault();
		     setEdit(!getEdit());
		   });
  $(".h1").click(makeCommandHandler("formatblock", "h1"));
  $(".h2").click(makeCommandHandler("formatblock", "h2"));
  $(".h3").click(makeCommandHandler("formatblock", "h3"));
  $(".pre").click(makeCommandHandler("formatblock", "pre"));
  $(".p").click(makeCommandHandler("formatblock", "p"));
  $(".insertunorderedlist").click(makeCommandHandler("insertunorderedlist", null));

  $(".link").click(function(event){
    // TODO: toggle link
    event.preventDefault();
    var selection = editor.contentWindow.getSelection();
    editor.contentWindow.document.execCommand("CreateLink", false, $.trim(selection.toString()));
  });

  $(editor.contentWindow.document).mousedown(function(event) {
					if (event.target.tagName == "A") {
					  setEdit(false);
					} else {
					  setEdit(true);
					}
				      });

  $(document).keydown(keydown);
  $(editor.contentWindow.document).keydown(keydown);
  $("#transcript")[0].contentWindow.document.designMode = "on";
  showSource(false);

  wiki = new Wiki();
  wiki.storage = wikiPath;
  wiki.auth = Auth;
  wiki.contentsChanged = load;
  wiki.init();
}

//## Command Handler constructor

function makeCommandHandler(command, value) {
  return function(event) {
    event.preventDefault();
    $("#editor")[0].contentWindow.document.execCommand(command, false, value);
  };
}

//## Keyboard Handler

function keydown(event) {
  var isControl = event.ctrlKey;
  var isAlt = event.alt || (!event.ctrlKey && event.metaKey);
  // console.log("crtl:" + event.ctrlKey + ", alt:" + event.altKey + ", meta:" + event.metaKey);
  if (event.keyCode == "\r".charCodeAt(0)) {
    return Platform.enterKey(event);
  }
  if (event.view == window && event.keyCode == 40) {
    event.preventDefault();
    nextSection();
    return false;
  }
  if (event.view == window && event.keyCode == 38) {
    event.preventDefault();
    previousSection();
    return false;
  }
  if (isAlt && (event.keyCode == "D".charCodeAt(0))) {
    event.preventDefault();
    doIt(event.target.ownerDocument);
    return false;
  }
  if (isAlt && (event.keyCode == "P".charCodeAt(0))) {
    event.preventDefault();
    printIt(event.target.ownerDocument);
    return false;
  }
  if (isControl && event.keyCode == "S".charCodeAt(0)) {
    event.preventDefault();
    save();
    return false;
  }
  return true;
}

//## Commands

function nextSection() {
  var editorWindow = getBody().ownerDocument.defaultView;
  var matched = $(getBody())
    .find(":header")
    .filter(function() { return this.offsetTop > editorWindow.scrollY; })
    .filter(":first");
  if (matched.length > 0) editorWindow.scroll(0, matched[0].offsetTop);
}

function previousSection() {
    var editorWindow = getBody().ownerDocument.defaultView;
    var matched = $(getBody())
    .find(":header")
    .filter(function() { return this.offsetTop < editorWindow.scrollY; })
    .filter(":last");
  if (matched.length > 0) editorWindow.scroll(0, matched[0].offsetTop);
}

function load(source) {
  document.title = wiki.title();
  window.evalCode = function(text) { return eval(text); }; // revert the evaluator
  $(".history")[0].href = "websvn/log.php?path=%2F" + wiki.fileName() + "&repname=" + wiki.storage;
  showHtmlFromSource(source);
  Body = getBody()
}

function save() {
  var body = getBody();
  var source = htmlToSource(body);
  wiki.save(source);

  showHtmlFromSource(source);
  display("Saved.\n");
}

//### Toggle show source

// The toggle policy is decided for saving from unexpected behavior by html
// editor (designMode). This is not actually for security reason. Security
// check is a future concern.

// Source to HTML: Almost free, you can enter any html expression.
// HTML to Source: Only certain elements and attributes are converted in standardize process.

function showSource(aBoolean) {
  isSource = aBoolean;
  var body = getBody();
  var source = htmlToSource(body);
  if(aBoolean) {
    $(body).empty();
    var pre = createElement("PRE", source);
    pre.style.background = "white";
    body.appendChild(pre);
    $("#editCommands").hide();
    $(".view").css("border-bottom", "1px solid #808080");
    $(".view").css("background", "");
    $(".source").css("border-bottom", "1px solid white");
    $(".source").css("background", "white");
  } else {
    showHtmlFromSource(source);
    $(".view").css("border-bottom", "1px solid white");
    $(".view").css("background", "white");
    $(".source").css("border-bottom", "1px solid #808080");
    $(".source").css("background", "");
  }
}

function getBody() {
  return $("#editor")[0].contentWindow.document.body;
}

function showHtmlFromSource(source) {
  $(getBody()).empty();
  $(sourceToHTML(source)).appendTo(getBody());
  isSource = false;
  $("#editCommands").show();
  setEdit(false);
}

// Convert to Source to a list of HTML element
// This converter is very line oriented!
function sourceToHTML(source) {
  if (source == null) return [];
  var lines = source.split("\n");
  var result = [];
  var match;
  while (lines.length > 0) {
    if (match = sourceParseMany(lines, sourceParseList)) { sourceToList(match, result); }
    else if (match = sourceParseMany(lines, sourceParseH3)) { sourceToHeader(match, result); }
    else if (match = sourceParseMany(lines, sourceParseH2)) { sourceToHeader(match, result); }
    else if (match = sourceParseMany(lines, sourceParseH1)) { sourceToHeader(match, result); }
    else if (match = sourceParseMany(lines, sourceParseP))  { sourceToHeader(match, result); }
    else if (match = sourceParseMany(lines, sourceParsePRE)) { sourceToPRE(match, result); }
    else { break; } // sourceParse error
  }
  return result;
}

function sourceToList(lines, result) {
  var newElement = document.createElement("UL");
  for (var i = 0; i < lines.length; i++) {
    var list = document.createElement("LI");
    list.innerHTML = lines[i].replace(/^\/\/- ?/, "");
    newElement.appendChild(list);
  }
  result.push(newElement);
}

function sourceToHeader(lines, result) {
  var mark = lines[0].match(/^\/\/(#*)/)[1];
  var level = mark.length;
  var text = "";
  for (var i = 0; i < lines.length; i++) {
    text += lines[i].replace(/^\/\/#* ?/, "") + "\n";
  }
  var newElement = document.createElement(level ? "H" + level : "P");
  newElement.innerHTML = text.replace(/\n*$/, "");
  result.push(newElement);
}

function sourceToPRE(lines, result) {
  var text = "";
  for (var i = 0; i < lines.length; i++) {
    text += lines[i] + "\n";
  }
  if (text.match(/^\s*$/)) return null;
  var newElement = createElement("PRE", text.replace(/^\n+|\n+$/g, "") + "\n");
  result.push(newElement);
}

function sourceParseMany(lines, func) {
  var result = [];
  while (lines.length > 0) {
    if (!func(lines[0])) break;
    result.push(lines.shift());
  }
  return result.length > 0 ? result : null;
}

// Parse one line. Return true if match
function sourceParseList(line) { return line.match(/^\/\/-/); }
function sourceParseH3(line) { return line.match(/^\/\/### /); }
function sourceParseH2(line) { return line.match(/^\/\/## /); }
function sourceParseH1(line) { return line.match(/^\/\/# /); }
function sourceParseP(line)  { return line.match(/^\/\/ /); }
function sourceParsePRE(line) { return !line.match(/^\/\/(#*|-)? /); }

// Convert to HTML to Source
function htmlToSource(body) {
  return $(standardizeChildren(body)).
    map(function() { return sourceElement(this); }).get().join("");
}

function sourceElement(element) {
  if (element.tagName == "PRE") return sourcePRE(element);
  if (element.tagName == "P") return sourceHeader(element, 0);
  if (element.tagName == "H1") return sourceHeader(element, 1);
  if (element.tagName == "H2") return sourceHeader(element, 2);
  if (element.tagName == "H3") return sourceHeader(element, 3);
  if (element.tagName == "UL") return sourceList(element);
  return sourceOther(element);
}

function sourcePRE(element) {
  return element.firstChild.nodeValue + "\n";
}

function sourceHeader(element, level) {
  var mark = "";
  var text = "";
  var lines = element.innerHTML.split("\n");
  for (var i = 0; i < level; i++) mark += "#";
  for (var i = 0; i < lines.length; i++) {
    text += "//" + mark + " " + lines[i] + "\n";
  }
  text += "\n";
  return text;
}

function sourceList(element) {
  var text = "";
  $(element).contents().each(function() { text += "//- " + this.innerHTML.replace(/\n/g, " ") + "\n"; });
  text += "\n";
  return text;
}

function sourceOther(element) {
  return "//? " + this.innerHTML + "\n";
}

// Trim unnecessary tags and attributes in the document.
// Answer a collection of standardized elements.
// H1 - H3, PRE, P, LI doesn't have nested element except <a> and <br>
// A and BR doesn't have any nested element.
// BR is converted to \n.
// UL only has LI
function standardizeChildren(element) {
  var elements = [];
  var newElement = null;
  var tagName = element.tagName;
  if (tagName == "BODY" || tagName == "DIV") tagName = "P";

  $(element).contents().
    map(function() { return standardizeElement(this); }).
    each(function() {
      var childTag = this.tagName;
      if (this.nodeType == document.TEXT_NODE ||
	  childTag == "IMG" ||
	  childTag == "A" ||
	  childTag == "STRONG" ||
	  childTag == "LI") {
	  if (newElement == null) {
            newElement = document.createElement(tagName);
	    elements.push(newElement);
	  }
	newElement.appendChild(this);
      } else {
	newElement = null;
	if (childTag != "BR") elements.push(this);
      }
    });

  var newElements = [];
  for (var i = 0; i < elements.length; i++) {
    if (elements[i].childNodes.length > 0) newElements.push(elements[i]);
  }

  return newElements;
}

// Traverse an element and return a list of standardize elements or an empty list.
function standardizeElement(element) {
  if (element.tagName == "SPAN") return standardizeSPAN(element);
  if (element.tagName == "PRE") return standardizePRE(element);
  if (element.tagName == "IMG") return standardizeIMG(element);
  if (element.tagName == "BR" ) return standardizeBR(element);
  if (element.tagName == "A" ) return standardizeA(element);
  if (element.tagName == "STRONG" ) return standardizeEmphasis(element);
  if (element.nodeType == document.TEXT_NODE) return standardizeTextNode(element);
  return standardizeChildren(element);
}

function standardizeSPAN(element) {
  return $(element).contents().
    map(function() { return standardizeElement(this); } ).get();
}

// A text element is trimmed and added two spaces surrounding it.
function standardizeTextNode(element) {
  var text = $.trim(element.nodeValue);
  if (text == "") return [];
  return [document.createTextNode(" " + text + " ")];
}

function standardizeA(element) {
  var newElement = createElement("A", element.text);
  newElement.setAttribute("href", element.getAttribute("href"));
  return [newElement];
}

function standardizeEmphasis(element) {
  return [createElement(element.tagName, textOfElement(element))];
}

function standardizeBR(element) {
  return document.createElement("BR");
}

// Trim first and last new lines and add \n at end.
function standardizePRE(element) {
  if (element.childNodes.length == 0) return [];
  var text = textOfElement(element).replace(/^\n+|\n+$/g, "") + "\n";
  if (text.match(/^\s*$/)) return [];
  return [createElement("PRE", text)]
}

function standardizeIMG(element) {
  var newElement = document.createElement("IMG");
  newElement.setAttribute("src", element.getAttribute("src"));
  return [newElement];
}

// Select input text and answer the selection.
function selectSource(ownerDocument) {
  var sourceRange = getSourceRange();
  var selection = ownerDocument.defaultView.getSelection();
  selection.removeAllRanges();
  selection.addRange(sourceRange);
  return selection;
}

// Return current selected text range, If length = 0, return a line enclosed the cursor.
function getSourceRange() {
  var editor = $("#editor")[0];
  var selection = editor.contentWindow.getSelection();
  var focusNode = selection.focusNode;
  var newRange = editor.contentWindow.document.createRange();

  // Obvious case
  if (!selection.isCollapsed) return selection.getRangeAt(0);
  if (selection.rangeCount == 0) return newRange;

  // In case of the cursor is between text node. TODO: fix it for deep nesting.
  if (focusNode.nodeType != document.TEXT_NODE) {
    var found = $(focusNode)
      .contents()
      .slice(0, selection.focusOffset + 1)
      .filter(function() { return this.nodeType == document.TEXT_NODE; });
    if (found.length == 0) return newRange;
    var foundText = found[found.length - 1];
    newRange.setStart(foundText, 0);
    newRange.setEnd(foundText, foundText.nodeValue.length);
    return newRange;
  }

  // In case of the cursor is middle of text.
  var text = focusNode.nodeValue;
  var end = selection.focusOffset;
  var index = 0;
  var start = 0;
  while (true) {
    start = index;
    index = text.indexOf("\n", index);
      if (index >= end || index < 0) {
        break;
      }
    index = index + 1;
  }
  end = index < 0 ? text.length : index;
  newRange.setStart(focusNode, start);
  newRange.setEnd(focusNode, end);
  return newRange;
}

function run() {
  var runLine = function(index, element) {
    var aString = textOfElement(element);
    var _window = element.ownerDocument.defaultView;
    try {
      element.style.border = "1px solid #000000";
      _window.scroll(0, element.offsetTop);
      var result = evalCode(aString.valueOf());
      if (result) display(result + "\n");
    } catch (e) {
      element.style.border = "1px solid #ff0000";
      throw e;
    }
  };

  $(getBody())
    .find("pre")
    .map(runLine);
  display(wiki.title() + ": done.\n");
}

// Flatten the element and extract a text
function textOfElement(element) {
  return $(element).contents().map(function(i, e) {
    if (e.nodeType == document.TEXT_NODE) { return e.nodeValue; }
    else if (e.tagName == "BR") { return "\n"; }
    else { return textOfElement(e); }
  }).get().join("");
}

function printString(anObject) {
  return anObject.toString();
}

function evalSelection(ownerDocument) {
  var selection = selectSource(ownerDocument);
  var text = textOfElement(selection.getRangeAt(0).cloneContents());
  try {
    var result = evalCode(text);
  } catch (e) {
    if (e.errorPos != undefined) {
      var selection = ownerDocument.defaultView.getSelection();
      if (selection.focusNode.nodeType == document.TEXT_NODE) {
	insertText(e.toString() + "->", ownerDocument, true, text.length - e.errorPos);
      } else {
	display(e.toString() + "->" + text.substring(e.errorPos, 100));
      }
    } else {
      display(e + "\n");
    }
    throw e;
  }
  console.log("RESULT: " + result);
  return result;
}

function doIt(ownerDocument) {
  var result = evalSelection(ownerDocument);
  if (result == undefined) return;
  display(printString(result) + "\n");
}

function printIt(ownerDocument) {
  var result = evalSelection(ownerDocument);
  if (result == undefined) return;
  insertText("\n => " + printString(result), ownerDocument, true, 0);
}

// Answer [node, posision] pair where offset from the ref position.
// If the node isn't text and offset >= 0, count characters from begin.
// If the node isn't text and offset < 0,  count characters from end.
/*
function findNode(node, ref, offset) {
  if (node.nodeType == document.TEXT_NODE) {
    var position = ref + offset;
    var focusText = node.nodeValue;
    if (0 <= position && position <= focusText.length) return [node, position];
    var parent = node.parentNode;
    var	index = null;
    for (var i = 0; i < parent.childNodes.length; i++) {
      if (parent.childNodes[i] == node) {
	index = i;
	break;
      }
      throw "Do not reach here.";
    }
    if (position < 0) findNode(parent, index, position);
    if (position > focusText.length) findNode(parent, index, focusText.length - position);
  }
  var length = findNodeTextLength(node);
  if (offset > 0) {
    findNode(node.parent, 0, )
  }
  if (length < 0 - offset)

  if (offset < 0 && ref > 0) findNode(node.childNodes[ref], index, position);
  if (offset < 0 && ref == 0) findNode(parent, index, position);
}

// Answer text length of the node recursively
findNodeTextLength(node) {
  if (node.nodeType == document.TEXT_NODE) return node.nodeValue.length;
  var length = 0;
  for (var i = 0; i < node.childNodes.length; i++) {
    length += findNodeTextLength(node.childNodes[i]);
  }
  return length;
}
*/

// Insert plain text at selected area in the document.
//- text: inserting text
//- ownerDocument: the document object
//- isSelected: true if the text should be selected.
//- backOffset:	the text is inserted at the position. Most end is zero.
function insertText(text, ownerDocument, isSelected, backOffset) {
  var selection = ownerDocument.defaultView.getSelection();

  var focusNode = selection.focusNode;
  var focusOffset = selection.focusOffset;
  var newRange = ownerDocument.createRange();

  if (focusNode.nodeType == document.TEXT_NODE) {
    focusOffset = focusOffset - backOffset;
    var focusText = selection.focusNode.nodeValue;
    var head = focusText.substring(0, focusOffset);
    var tail = focusText.substring(focusOffset);
    focusNode.nodeValue = head + text + tail;

    newRange.setStart(focusNode, focusOffset);
    newRange.setEnd(focusNode, focusOffset + text.length);
  } else {
    var textNode = ownerDocument.createTextNode(text);
    var nextElement = focusNode.childNodes[focusOffset];
    focusNode.insertBefore(textNode, nextElement);

    newRange.setStart(textNode, 0);
    newRange.setEnd(textNode, text.length);
  }

  selection.removeAllRanges();
  selection.addRange(newRange);
  if (!isSelected) selection.collapseToEnd();
  ownerDocument.defaultView.focus();
}

function getEdit() {
    return isEdit;
}

function display(aString) {
  var _document = $("#transcript")[0].contentWindow.document;
  var element = _document.getElementById("transcript_body");
  var textNode = _document.createTextNode(aString);
  element.appendChild(textNode);
  _document.defaultView.scroll(0, 100000);
}

// Library functions
function include(name) {
  var contents = wiki.readUrl(wiki.urlFor(name));
  var result = evalCode(contents);
  display(name + ": done.\n");
  return result;
}

if (jQuery.browser.safari && !Element.prototype.getBoundingClientRect) {
  Element.prototype.getBoundingClientRect = function() {
    var coords = { left: 0, top: 0, width: this.offsetWidth, height: this.offsetHeight };
    var element = this;
    while (element) {
      coords.left += element.offsetLeft;
      coords.top += element.offsetTop;
      element = element.offsetParent;
    }
    return coords;
  };
}

function setEdit(aBoolean) {
    isEdit = aBoolean;
    if (aBoolean) {
      Platform.designMode($("#editor")[0], "On");
      $("#td_editor").css("border", "2px solid #f08080");
    } else {
      Platform.designMode($("#editor")[0], "Off");
      $("#td_editor").css("border", "1px solid #808080");
    }
}

// Utility function to build a HTML element.
function createElement(tagName, aString) {
  var newElement = document.createElement(tagName);
  var textNode = document.createTextNode(aString);
  newElement.appendChild(textNode);
  return newElement;
}

// Platform dependent configuration

function Platform_base () {};
function Platform_ie () {};
function Platform_safari () {};

Platform_ie.prototype = new Platform_base();
Platform_safari.prototype = new Platform_base();

Platform_base.prototype.designMode = function(iframe, aString) {
  iframe.contentWindow.document.designMode = aString;
};

Platform_ie.prototype.designMode = function(iframe, aString) {
  var _document = iframe.contentWindow.document;
  var html = _document.body.innerHTML;
  _document.designMode = aString;
  setTimeout(function() {
	       _document.body.innerHTML = html;
	     }, 100);
};

Platform_base.prototype.enterKey = function(event) { return true };

Platform_safari.prototype.enterKey = function(event) {
  var selection = event.target.ownerDocument.defaultView.getSelection();
  if (selection.focusNode.parentNode.tagName != "PRE") return true;

  // TODO: If selected point of the text is the end of PRE node, you have
  // to press enter twice. That's why last \n is ignores by the browser.
  insertText("\n", event.target.ownerDocument, false, 0);
  return false;
};

if ($.browser.msie) {
  Platform = new Platform_ie();
} else if ($.browser.safari) {
  Platform = new Platform_safari();
} else {
  Platform = new Platform_base();
}

// console.log

if (!window.console) {
  window.console = new Object();
  window.console.log = function() {};
}

/* Remove me later

// Serialize inner elements
function serialize(element) {
  return serializeElements($(element).contents());
}

function serializeElements(elements) {
  return $(elements).map(function(i) {
    if (this.nodeType == document.TEXT_NODE) { return this.nodeValue }
    else if (this.tagName == "PRE") { return serializePRE(this) }
    else if (this.tagName == "A") { return serializeA(this) }
    else if (this.tagName == "IMG") { return serializeIMG(this) }
    else { return serializeParagraph(this); }
  }).get().join("");
}

function serializeParagraph(element) {
  return text = "<" + element.tagName.toLowerCase() + ">" +
    serializeElements($(element).contents()) +
    "</" + element.tagName.toLowerCase() + ">\n";
}

function serializePRE(element) {
  var text = serializeElements($(element).contents());
  return "<pre>\n" +
    text +
    (text[text.length - 1] == "\n" ? "" : "\n") +
    "</pre>\n\n";
}

function serializeA(element) {
  return '<a href="' + element.text + '">' +
    element.text +
    "</a>";
}

function serializeIMG(element) {
  return '<img src="' + element.src + '" />';
}

*/

