/*
*
* Pixastic Editor for Mozilla Labs Ubiquity
* 
* Enables commands:
*  edit-image         Execute this command and click on any image on the page to edit that image.
*  edit-screenshot    Execute this command to grab a picture of the current page and edit that image.
*
* Upon execution of the commands, one or more JavaScript files are injected into the current page.
* When these files are loaded, the Pixastic Editor is fired up which takes care of loading CSS and 
* images and setting up the editor in an iframe on top of the page. Note that the editor JavaScript
* is running in the scope of the main window, not the iframe.
*
* Right now, the files are fetched from www.pixastic.com but it is the goal to have them bundled inside 
* Ubiquity eventually.
*
* Pixastic Editor has further been enhanced with a MD5 hash function (in PixasticEditor.MD5) 
* and Flickr API functions (in PixasticEditor.Flickr). 
* An extra action tab has also been added to the "Done" UI group to use this functionality.
* All the Pixastic extending can be found here in the function ubiquifyPixastic().
*
*/

(function() {

//var baseUrl = "resource://ubiquity/scripts/pixastic-editor/";
var baseUrl = "http://www.pixastic.com/ubiquity/";
//var baseUrl = "http://pixastic/lib/ubiquity/";

// inject the needed JavaScript file(s) into the page
function loadPixasticFiles( context, callback ){
  var doc = CmdUtils.getDocumentInsecure();
  if (doc._pixasticLoading) return;


  // TODO: everything is now in a single JS file, so we no longer need this 
  // loading queue.

  var scripts = [
    "editor.min.js"
    //"editor-all.js.php"
  ];

  if (!doc._pixasticLoaded) {
    doc._pixasticLoading = true;

    function loadNextScript() {
      var next = scripts.shift();
      var s = doc.createElement("script");
      s.src = baseUrl + next;
      s.addEventListener("load", function() {
        if (scripts.length) {
          loadNextScript();
        } else {
          doc._pixasticLoaded = true;
          doc._pixasticLoading = false;
          callback();
        }
      }, false);
      doc.getElementsByTagName("head")[0].appendChild(s);
    }
    loadNextScript();
  } else {
    callback();
  }
}

// returns the data of an img element as a data URI
// by painting the image on a canvas and calling toDataURL()
function getImageDataUrl(img) {
  var hWin = CmdUtils.getHiddenWindow();
  var canvas = hWin.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
  canvas.width = img.width;
  canvas.height = img.height;    
  canvas.getContext("2d").drawImage(img, 0, 0);
  return canvas.toDataURL();
}

// extend PixasticEditor with some extra functionality 
// only available when running from Ubiquity
function ubiquifyPixasticEditor() {

  var win = CmdUtils.getWindowInsecure();
  var doc = CmdUtils.getDocumentInsecure();
  var PixasticEditor = win.PixasticEditor;
  var PE = PixasticEditor;
  var $ = PE.$;

  // Post the current image data to a specified url
  // fieldName is the name of the image data field
  // extraData is an optional object containing key:value pairs with extra form data fields
  // Based on http://blog.footle.org/2007/07/31/binary-multipart-posts-in-javascript/
  PE.postImage = function(url, filename, fieldname, extraData, callback, onprogress) {

	var imgData = win.atob(PixasticEditor.getDataURI().substring(13 + ("image/png").length));

	var bound = "--image-boundary-" + (new Date().getTime()) + "--";

	var startStream = Components.classes["@mozilla.org/io/string-input-stream;1"].createInstance(Components.interfaces.nsIStringInputStream);

	var data = "--" + bound + "\r\n" +
          "Content-Disposition: form-data; name=\"" + fieldname + "\"; filename=\"" + filename + "\"\r\n" +
          "Content-Type: image/png\r\n\r\n";

	startStream.setData(data, data.length);

	var binStream = Components.classes["@mozilla.org/binaryoutputstream;1"].createInstance(Components.interfaces.nsIBinaryOutputStream);
	var dataStream = Components.classes["@mozilla.org/storagestream;1"].createInstance(Components.interfaces.nsIStorageStream);
	dataStream.init(4096, imgData.length, null);
	binStream.setOutputStream(dataStream.getOutputStream(0));
	binStream.writeBytes(imgData, imgData.length);
	binStream.close();

	// write out the rest of the form to another string input stream
	var endStream= Components.classes["@mozilla.org/io/string-input-stream;1"].createInstance(Components.interfaces.nsIStringInputStream);
        if (extraData) {
          data = "\r\n";
          for (var a in extraData) {
            if (extraData.hasOwnProperty(a)) {
              data += "\r\n--" + bound + "\r\n" + 
                "Content-Disposition: form-data; name=\"" + a + "\"\r\n\r\n" + extraData[a];
            }
          }
          data += "\r\n--" + bound + "--\r\n";
        } else {
          data = "\r\n--" + bound + "--\r\n"; 
        }
	endStream.setData(data, data.length);

        var finalStream = Components.classes["@mozilla.org/io/multiplex-input-stream;1"].createInstance(Components.interfaces.nsIMultiplexInputStream);
        finalStream.appendStream(startStream);
        finalStream.appendStream(dataStream.newInputStream(0));
        finalStream.appendStream(endStream);

        var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]  
                     .createInstance(Components.interfaces.nsIXMLHttpRequest); 
	req.open("POST", url, true);
	req.setRequestHeader("Accept", "*/*, application/xml");
	req.setRequestHeader("Content-type", "multipart/form-data; boundary=" + bound);
	req.setRequestHeader("Content-length", finalStream.available());
	req.setRequestHeader("User-Agent", "PixasticEditor/1.0.0");

        var onError = function(ev) {
          if (callback)
            callback(false, "Error posting image");
        }
        req.addEventListener("error", onError, false); 

        var onProgressUpdate = function() {
          if (onProgress && evt.lengthComputable) {
            onProgress(evt.loaded / evt.total);
          }
        }
        //req.upload.addEventListener("progress", onProgressUpdate, false); 

        req.onreadystatechange = function (ev) {  
          if (req.readyState == 4) {  
            if(req.status == 200) {
              if (callback) 
                callback(true, req.responseText);
            } else {
              if (callback) 
                callback(false, "Error posting image. HTTP status " + req.status);
            }
          }  
        };  
	req.send(finalStream);
  }

  // Flickr API integration
  PE.Flickr = (function() {
	var frob;
	var token;
	var authName = "";
	var apikey = "d88224854654e1dadca3c5682d2095cc";
	var secret = "c69404235a22453f";

	var Flickr = (function() {
		var callCount = 0;
		function sendRequest(content, callback) {
			callCount++;
			content = content || {};
			content["jsoncallback"] = "PixasticEditor.Flickr._callbacks.fn_" + callCount
			content["format"] = "json";
			var url = Flickr.makeUrl("http://www.flickr.com/services/rest/", content);
			var script = doc.createElement("script");
			var fncFlickr = function(response) {
				doc.body.removeChild(script);
				if (typeof callback == "function")
					callback(response);
			}
			PixasticEditor.Flickr._callbacks["fn_" + callCount] = fncFlickr;
			script.setAttribute("type", "text/javascript");
			doc.body.appendChild(script);
			script.src = url;
		}
	
		return {
			makeUrl : function(baseUrl, content) {
				sig = Flickr.makeSig(content);
	
				var argsObj = {};
				argsObj["api_key"] = apikey;
	
				for (var a in content) {
					if (content.hasOwnProperty(a)) {
						argsObj[a] = content[a];
					}
				}
		
				argsObj["api_sig"] = sig;
		
				var url = baseUrl
				var first = true;
				for (var a in argsObj) {
					if (argsObj.hasOwnProperty(a)) {
						url += (first ? "?" : "&") + a + "=" + encodeURIComponent(argsObj[a]);
						first = false;
					}
				}
				return url;
			},
			makeSig : function(content) {
				var argsArr = ["api_key"];
				var argsObj = {};
				argsObj["api_key"] = apikey;
				for (var a in content) {
					if (content.hasOwnProperty(a)) {
						argsArr.push(a);
						argsObj[a] = content[a];
					}
				}
				argsArr = argsArr.sort();
				var sig = secret;
				for (var i=0;i<argsArr.length;i++) {
					sig += argsArr[i] + argsObj[argsArr[i]];
				}
				sig = PixasticEditor.MD5.hex_md5(sig);
				return sig;
			},
			callMethod : function(content, callback) {
				sendRequest(content, callback);
			}
	
		}
	})();


	// publicly available methods
	return {
		auth : function() {
			PixasticEditor.Flickr.getFrob(function(res) {
				var authUrl = Flickr.makeUrl("http://www.flickr.com/services/auth/", {perms : "write", frob : frob});
				var authWin = context.chromeWindow.open(authUrl, "pixasticeditor-flickr-auth");
			});
		},
		getFrob : function(callback) {
			Flickr.callMethod({method:"flickr.auth.getFrob"}, 
				function(res) {
					if (res.stat == "ok") {
						frob = res.frob._content;
						if (typeof callback == "function")
							callback(res);
					}
				}
			);
		},
		checkAuth : function(callback) {
			if (token) {
				if (callback) {
					callback({
						stat : "ok"
					});
				}
			} else {
				PixasticEditor.Flickr.getToken(callback);
			}
		},
		getAuthName : function() {
			return authName;
		},
		getToken : function(callback) {
			Flickr.callMethod({method:"flickr.auth.getToken", frob:frob}, 
				function(res) {
					if (res.stat == "ok") {
						token = res.auth.token._content;
						authName = res.auth.user.fullname;
					}
					if (typeof callback == "function")
						callback(res);
				}
			);
		},
		uploadImage : function(title, tags, description, callback, onProgress) {
			var params = {};
			params["auth_token"] = token;
			params["title"] = title;
			params["tags"] = tags;
			params["description"] = description;
			var sig = Flickr.makeSig(params);
			params["api_key"] = apikey;
			params["api_sig"] = sig;
			PixasticEditor.postImage(
				"http://api.flickr.com/services/upload/", 
				"image.png", "photo", params, callback, onProgress
			);
		},
		_callbacks : {}
	}
  })();


  // MD5 hashing
  // See http://rcrowley.org/2007/11/15/md5-in-xulrunner-or-firefox-extensions/

  var _md5 = null;
  try {
    _md5 = Cc['@mozilla.org/security/hash;1'].createInstance(Ci.nsICryptoHash);
  } catch (err) {
    Components.utils.reportError(err);
  }
  var md5 = function(str) {
    if (null == _md5) {
      return '';
    }

    // Build array of character codes to MD5 
    var arr = []; 
    var ii = str.length; 
    for (var i = 0; i < ii; ++i) { 
      arr.push(str.charCodeAt(i)); 
    }
    _md5.init(Ci.nsICryptoHash.MD5);
    _md5.update(arr, arr.length);
    var hash = _md5.finish(false);

    // Unpack the binary data bin2hex style 
    var ascii = []; 
    ii = hash.length; 
    for (var i = 0; i < ii; ++i) {
      var c = hash.charCodeAt(i); 
      var ones = c % 16; 
      var tens = c >> 4; 
      ascii.push(String.fromCharCode(tens + (tens > 9 ? 87 : 48)) + String.fromCharCode(ones + (ones > 9 ? 87 : 48)));
    }
    return ascii.join(''); 
  };
  PE.MD5 = {
    hex_md5 : md5
  }


  // Add the Flickr action to the editor UI
  PE.UI.addAction("done", "flickrupload", {
    title : "Upload to <span style='font-weight:bold;font-family:arial;font-size:16px;color:#0063DC;'>flick<span style='color:#FF0084'>r</span></span>",
    enabled : function() {
      return !!PE.Flickr;
    },
    content : function($ctr) {
      var doc = PE.getDocument();
      PE.Flickr.checkAuth(function(res) {
        if (res.stat == "ok")
          flickrAuthed();
        else
          flickrNotAuthed();
      });

      function flickrAuthed() {
        var $text = $("<div />", doc)
          .addClass("action-output-text")
          .html("Authorized as: " + PE.Flickr.getAuthName());

        var $buttonCtr = $("<div></div>", doc);
        var $uploadButton = $("<button></button>", doc)
          .html("Upload image")
          .appendTo($buttonCtr)
        $ctr.append($text);

        var inputTitle = PE.UI.makeTextInput("Image title", null, "flickrtitle", 0, function(){return "My image";}, null);
        var inputTags = PE.UI.makeTextInput("Tags", null, "flickrtags", 0, function(){return "";}, null);
        var textDescription = PE.UI.makeTextArea("Description", null, "flickrdesc", 0, function(){return "";}, null);
        $ctr.append(inputTitle.container, inputTags.container, textDescription.container, $buttonCtr);

        $uploadButton.click(function() {
          var callback = function(success, message) {
            PE.toggleWaiting(false);
            if (success)
              PE.messageBox("Image successfully uploaded to Flickr", "Upload successful");
            else
              PE.messageBox("Something bad happened when trying to upload your image to Flickr: " + message, "Oops!");
          }
          PE.toggleWaiting(true);
          PE.Flickr.uploadImage(inputTitle.valueField.val(), inputTags.valueField.val(), textDescription.valueField.val(), callback);
        });
      }

      function flickrNotAuthed() {
        var $authCtr = $("<div />", doc).appendTo($ctr);
        $("<div />", doc)
          .addClass("action-output-text")
          .html("If you have a Flickr account you can now upload your image to Flickr. In order to do so you will need login and give access to your  account first. Click the button below to open an authentication window.")
          .appendTo($authCtr);
        var $buttonCtr = $("<div></div>", doc).appendTo($authCtr);
        var $authButton = $("<button></button>", doc)
          .html("Authenticate")
          .appendTo($buttonCtr)
        var checkButtonAdded = false;
        $authButton.click(function() {
          PE.Flickr.auth();
          if (!checkButtonAdded) {
            checkButtonAdded = true;
            var $text = $("<div />", doc)
              .addClass("action-output-text")
              .html("Now click the button below when you have authorized access to your Flickr account.");
            var $buttonCtr = $("<div></div>", doc);
            $authCtr.append($text, $buttonCtr);
            var $checkButton = $("<button></button>", doc)
              .html("I have authenticated!")
              .appendTo($buttonCtr);
            $checkButton.click(function() {
              PE.Flickr.checkAuth(function(res) {
                if (res.stat == "ok") {
                  $authCtr.remove();
                  flickrAuthed();
                } else {
                  PE.messageBox("Authorization check failed. Try authorizing again.");
                } 
              });
            });
          }
        });
      }
    }
  }, 2);


}


// Finally, create Ubiquity commands

// Image editing command
CmdUtils.CreateCommand({ 
  name: "edit-image",
  icon: "http://www.pixastic.com/favicon.ico",
  homepage: "http://www.pixastic.com/",
  author: { name: "Jacob Seidelin", email: "jseidelin@nihilogic.dk"},
  license: "MIT",
  description: "Edit images with the Pixastic editor",
  help: "Execute the command and wait for the notification that Pixastic has been loaded, then click any image on the page to load the image editor.",
  takes: {/* "input": noun_arb_text */ },
  execute: function(input) {
    displayMessage("Loading Pixastic, please wait...");
    loadPixasticFiles( context, function(){
      var doc = CmdUtils.getDocumentInsecure();
      var win = CmdUtils.getWindowInsecure();
      var onclick = function(e) {
        doc.removeEventListener("click", onclick, true);
        var el = e.target.wrappedJSObject;
        if (el.tagName.toLowerCase() == "img" || el.tagName.toLowerCase() == "canvas") {
          if (el.tagName.toLowerCase() == "img")
            var data = getImageDataUrl(el);
          else
            var data = el.toDataURL();
          ubiquifyPixasticEditor();
          win.PixasticEditor.loadDataURI(el, data, el.width, el.height, baseUrl, ["pixastic.ubiquity.css"]);
          e.preventDefault();
          e.stopPropagation();
        } else {
          displayMessage("Image editing cancelled.");
        }
      }
      doc.addEventListener("click", onclick, true);
      displayMessage("Pixastic loaded. Click any image on the page to launch the image editor.");
    });  
  }
});

// Screenshot editing command
CmdUtils.CreateCommand({ 
  name: "edit-screenshot",
  icon: "http://www.pixastic.com/favicon.ico",
  homepage: "http://www.pixastic.com/",
  author: { name: "Jacob Seidelin", email: "jseidelin@nihilogic.dk"},
  license: "MIT",
  description: "Edit a screenshot of the current window with the Pixastic editor",
  help: "Simply execute this command to grab a screenshot of the current page and load the Pixastic image editor.",
  takes: {/* "input": noun_arb_text */ },

  execute: function(input) {
    var doc = CmdUtils.getDocumentInsecure();
    var win = CmdUtils.getWindowInsecure();

    var hWin = CmdUtils.getHiddenWindow();
    var shot = hWin.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
    var width = win.innerWidth;
    var height = win.innerHeight;
    shot.mozOpaque = true;
    shot.width = width;
    shot.height = height;
    shot.getContext("2d").drawWindow(win, 0, 0, width, height, "rgb(255,255,255)");
    var data = shot.toDataURL("image/png");

    loadPixasticFiles( context, function(){
      ubiquifyPixasticEditor();
      win.PixasticEditor.loadDataURI(null, data, width, height, baseUrl, ["pixastic.ubiquity.css"]);
    });  
  }
});

})();
