// ==UserScript==
// @name          Flickr Batch Operations Enhancer
// @namespace     http://lachnlone.no-ip.info/greasemonkey
// @description	  Adds several features to Flickr's batch operations
// @include       http://www.flickr.com/photos_batch_operations.gne*
// @include       http://flickr.com/photos_batch_operations.gne*
// ==/UserScript==

/*
	Flickr Batch Operations Enhancer v0.4.666
	(c) 2005 Lachie
	http://lachblog.blogspot.com/
	http://www.flickr.com/photos/lachie/
	
	tweaked by mykle to work in FF 1.5
	removed the date, permissions, and license stuff because
	i was too lazy to make it work proper like. even though some
	of it probably just works already.	
	http://convulse.net/

	Credits
	http://livehttpheaders.mozdev.org/                  - invaluable for sniffing the POSTs
	http://persistent.info/greasemonkey/gmail.user.js   - nicked a bit from this script
	http://brevity.org/greasemonkey/                    - Neil Kandalgaonkar for the Flickr API bits, and beautiful clean example code (Lickr)	
*/



(function() {

/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }

/*
 * Perform a simple self-test to see if the VM is working
 */
function md5_vm_test()
{
  return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}

/*
 * Calculate the MD5 of an array of little-endian words, and a bit length
 */
function core_md5(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << ((len) % 32);
  x[(((len + 64) >>> 9) << 4) + 14] = len;

  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;

  for(var i = 0; i < x.length; i += 16)
  {
    var olda = a;
    var oldb = b;
    var oldc = c;
    var oldd = d;

    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
  }
  return Array(a, b, c, d);

}

/*
 * These functions implement the four basic operations the algorithm uses.
 */
function md5_cmn(q, a, b, x, s, t)
{
  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}

/*
 * Calculate the HMAC-MD5, of a key and some data
 */
function core_hmac_md5(key, data)
{
  var bkey = str2binl(key);
  if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);

  var ipad = Array(16), opad = Array(16);
  for(var i = 0; i < 16; i++)
  {
    ipad[i] = bkey[i] ^ 0x36363636;
    opad[i] = bkey[i] ^ 0x5C5C5C5C;
  }

  var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
  return core_md5(opad.concat(hash), 512 + 128);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function bit_rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert a string to an array of little-endian words
 * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
 */
function str2binl(str)
{
  var bin = Array();
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < str.length * chrsz; i += chrsz)
    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
  return bin;
}

/*
 * Convert an array of little-endian words to a string
 */
function binl2str(bin)
{
  var str = "";
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < bin.length * 32; i += chrsz)
    str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
  return str;
}

/*
 * Convert an array of little-endian words to a hex string.
 */
function binl2hex(binarray)
{
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i++)
  {
    str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
           hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
  }
  return str;
}

/*
 * Convert an array of little-endian words to a base-64 string
 */
function binl2b64(binarray)
{
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i += 3)
  {
    var triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16)
                | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
                |  ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
    for(var j = 0; j < 4; j++)
    {
      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
    }
  }
  return str;
}



	 //------------------------------------------------------------------------
	 // constants
	 // http status constants
	 var OK = 200

	 // xmlhttprequest readystate
	 var COMPLETE = 4

	 var API_KEY = '6c1d721772b6ded94c913bfe0051d480';
	 var SHARED_SECRET = '9d9db8139f5fd6f8';

	 var TOKEN = '';
	 var NSID = '';
	 var DEBUG = false;

	 var MAX_PICS_TO_GROUP = 5;

	// nicked from http://persistent.info/greasemonkey/gmail.user.js
	function getObjectMethodClosure(object, method) {
		return function() {
			return object[method](); 
		}
	}

	function getObjectMethodClosure1(object, method) {
		return function(arg) {
			return object[method](arg); 
		}
	}
	function getObjectMethodClosure2(object, method) {
		return function(arg,arg2) {
			return object[method](arg,arg2); 
		}
	}
	
	function getMethodSig(method, args)
	{
		var data = new Array();		
		var names = new Array();
		var sig = SHARED_SECRET;		
		
		data['method'] = method;	
		names.push('method');
		for (var key in args) {
			data[key] = args[key];
			names.push(key);
		}		
		names.sort();
		for (i in names) {
		sig += names[i] + data[names[i]];
		}		
		return hex_md5(sig);
	}

	function centerbox(s)
	{
		/*var str = "<div align=center><div style='text-align: left;'>";
		str += s;
		str += '</div></div>';
		return str;*/
		return s;
	}

	// Shorthand
	var newNode = getObjectMethodClosure1(document, "createElement");
	var newText = getObjectMethodClosure1(document, "createTextNode");
	var getNode = getObjectMethodClosure1(document, "getElementById");
	var getTags = getObjectMethodClosure1(document, "getElementsByTagName");
	
	
	//this code blatantly lifted from the Flickr Super Batch scripts
	//http://webdev.yuan.cc/
	var status_msg_container = newNode('div');
	status_msg_container.style.left = '50%';
	status_msg_container.style.width = '400px';
	//status_msg_container.style.height = '400px';
	status_msg_container.style.zIndex = 60000;
	//status_msg_container.style.overflow = 'visible';
	status_msg_container.style.position = 'absolute';
	status_msg_container.style.display = 'none';
	//status_msg_container.style.textAlign = 'center';
	status_msg_container.innerHTML = '<div id="status_msg" style="position:relative;left: -50%;top:100px;background:#d5eaff;padding:10px;font:bold 12px Arial, Helvetica, sans-serif; color:#000000;border:solid 1px #ccddee;"></div>';
	
	var ip = xpath_single_node(document,'//p[@class="topnavi"][1]');		
	ip.parentNode.appendChild(status_msg_container);
	
	
	var status_msg = getNode('status_msg');
	status_msg.submitfunc = function() {};
	status_msg.on = false;
	status_msg.show = function(msg) {
		this.on = true;
		this.innerHTML = '<img src="http://www.flickr.com/images/pulser2.gif" style="vertical-align:middle;margin-right:4px;border:0px #ffffff" />';
		this.innerHTML += msg;
		status_msg_container.style.display = 'block';
	}
	status_msg.msgbox = function(msg) {
		this.on = true;
		this.innerHTML = centerbox(msg);
		this.innerHTML += "<div align=center>[<a id='closeBox'>close</a>]</div>";
		a = getNode('closeBox');
		a.addEventListener('click',this.clicky,true);
		status_msg_container.style.display = 'block';
	}
	status_msg.hide = function() {
	    status_msg.on = false;
	    status_msg_container.style.display = 'none';
			status_msg_container.style.textAlign = 'left';
	}
	status_msg.prompt = function(msg, fn) {
		status_msg.submitfunc = fn;
		this.on = true;
		this.innerHTML = centerbox(msg);		
		var form = newNode('form');
		form.addEventListener('submit',this.entered,true);
		var input = newNode('input');		
		input.id = 'woowoo';
		input.type = 'text';
		form.appendChild(input);
		input = newNode('input');
		input.type = 'submit';
		input.value = 'ok';
		form.appendChild(input);
		this.appendChild(form);
		status_msg_container.style.display = 'block';
				
	}
	status_msg.entered = function(event) {
		var input = getNode('woowoo');
		status_msg.submitfunc(input.value);
		event.preventDefault();
		event.returnValue = false;
		event.cancel = true;
		return false;
	}
	status_msg.clicky = function(event) {
		status_msg.hide();
		event.preventDefault();
		event.returnValue = false;
		event.cancel = true;
		return false;
	}


	// display status
	function update_status() {
		var s = "<strong>Sending photos to group</strong<br>" + this.status["sent"] + " out of " + this.status["total"];
		if (this.status["failed"] > 0) {
			s += "<br>(" + this.status["failed"] + " fux0red)";
		}
		status_msg.show(s);
		if ((this.status["sent"] + this.status["failed"]) >= this.status["total"]) {
			status_msg.hide();
		}
	}

	function do_post( cb_method, url, referer, data ) {
		var object = this;

		GM_xmlhttpRequest({
			method  : "POST"  ,
			url     : url     ,
			data    : data    ,
			onload  : function() { object[cb_method](req) },
			headers : {
				'Referer'      : referer,
				'Content-Type' : 'application/x-www-form-urlencoded'
			}
		});
	}

	function do_get( object, cb_method, url, referer ) {
		var object = this;

		GM_xmlhttpRequest({
			method  : "POST"  ,
			url     : url     ,
			onload  : function(d) { object[cb_method](d) },
			headers : {
				'Referer'      : referer,
			}
		});

	}


	// lifted from lickr.js - with mods

	function xpath_single_node(context_node, xpath) {
		return  document.evaluate( 
				xpath,                              
				context_node, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
				).singleNodeValue;
	}



	// flickr api 

	function do_req( method, proc_request, url, referer, data ) {
		var headers = new Object();
		var details = {
			method    : method,
			onload    : function(d) { proc_request(d) },
			url       : url,
			header    : headers
		};

		if (referer != null)
			headers['Referer'] = referer;

		if (data != null) {
			headers['Content-Type'] = 'application/x-www-form-urlencoded';
			details['data']         = data;
		}

		GM_xmlhttpRequest( details );
	}

	function procException(msg, req) {
		this.msg = msg
			this.req = req
	}


	// a proc just spins around waiting for the thing to succeed or fail
	// then calls a callback, if we got 200 OK message.
	function make_proc(op_name, ok_cb, fail_cb) {

		return function(req) { 

			try {
				// init progress
				document.body.style.cursor = 'progress';

				if (req.readyState != COMPLETE) {
					return;
				}

				// if (alert_response) { alert(req.responseText); }

				if( req.status != OK ) {
					throw new procException( op_name + " request status was '" + req.status + "'", req )
				}

				ok_cb(req);

			} catch(e) {

				// clean up progress
				document.body.style.cursor = 'default';


				if (e instanceof procException) {
					if( fail_cb != null )
						fail_cb( e );
					else {
						GM_log( e.msg );
						if (DEBUG) {
							GM_log(e.req.responseText);
						}
					}
				} else {
					throw(e);
				}
			}

			// clean up progress

			document.body.style.cursor = 'default';
		}
	}


	// this is wraps the spinning proc like above,
	// except it parses the flickr api response a little before deciding all is well,
	// and passing control to the all-is-well callback
	function make_flickr_api_proc(op_name, ok_cb, fail_cb) {

		function parse_and_ok_cb(req) {
			var w = new XPCNativeWrapper(window, "DOMParser()"); //DOMParser();
			var parser = new w.DOMParser();									
			GM_log(req.responseText);
			var doc = parser.parseFromString( req.responseText, 'text/xml' );
			var rsp = doc.getElementsByTagName('rsp').item(0);

			// var rsp = req.responseXML.getElementsByTagName('rsp').item(0);

			if (rsp == null) {
				throw new procException( "Could not understand Flickr's response.", req );
			}

			var stat = rsp.getAttribute("stat");
			if (stat == null) {
				throw new procException( "Could not find status of Flickr request", req);
			}

			if (stat != 'ok') {
				if (stat == 'fail') {
					var err_node = rsp.getElementsByTagName('err').item(0);
					var err_msg = err_node.getAttribute("msg");
					throw new procException( err_msg, req );
				} else {
					throw new procException("Unknown error status: '" + stat + "'", req)
				}
			}

			ok_cb(req, rsp);
		}

		return make_proc(op_name, parse_and_ok_cb, fail_cb);
	}


	// construct a flickr api request, with method and args, 
	// if that worked, call callback with request object.
	function flickr_api_call( method, args, ok_cb, fail_cb) {

		//GM_log(args);		
		//if (signed) {
			//args['api_sig'] = getMethodSig(args);
		//}
		var http_method = args['http_method']
		http_method = ( http_method ? http_method : 'GET' )
		delete args['http_method']
		
		if (args['auth_token'] || args['mini_token']) {
			args['api_sig'] = getMethodSig(method, args);
		} else {
			GM_log('not signing: ' + method);
		}
	
		var url = 'http://www.flickr.com/services/rest/?api_key=' + API_KEY;
		url += '&method=' + encodeURIComponent(method);

		for (var key in args) {
			url += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(args[key])			
		}

		var proc = make_flickr_api_proc( method, ok_cb, fail_cb )

		

		do_req(http_method, proc, url, null, null)
	}

	// end of section lifted from lickr.js
	// -----------------------------------------------


	function inject_css( css ) {
		var style_el = xpath_single_node( document, '//head[1]/style[1]' );
		if( ! style_el ) {
			var style_el = newNode( 'style' );
			style_el.setAttribute("type", 'text/css');
			style_el.innerHTML = '';

			xpath_single_node( document, '//head[1]' ).appendChild( style_el );
		}

		style_el.innerHTML += css;
	}


	// make an anchor
	function mk_a(text,fn) {
		var a = newNode('a');	
		a.setAttribute('href','#');
		a.addEventListener('click',fn,true);
		//a.onclick = fn
		a.appendChild( newText(text) );

		return a;
	}
	
	function link(text,href) {
		var a = newNode('a');
		a.setAttribute('href',href);
		a.appendChild( newText(text) );

		return a;
	}


	// Date utils
	function split_to_int( sep, str ) {
		var bits = str.split(sep);
		for( var i=0; i<bits.length; i++ ) {
			bits[i] = parseInt(bits[i]);
		}
		return bits;
	}

	// parses and validates the date from a date and a time string
	function get_date( date_str, time_str ) {
		var dt;

		try {
			var date_bits = split_to_int( '/', date_str );
			var time_bits = split_to_int( ':', time_str );

			dt = new Date();

			dt.setYear(  date_bits[2] );
			dt.setMonth( date_bits[0]-1 );
			dt.setDate( date_bits[1] );

			dt.setHours( time_bits[0] );
			dt.setMinutes( time_bits[1] );
			dt.setSeconds( time_bits[2] );

			if(  date_bits[0] != dt.getMonth()+1 
				|| date_bits[1] != dt.getDate()
				|| date_bits[2] != dt.getFullYear()
			) throw new Error("couldn't parse date");

			if(  time_bits[0] != dt.getHours() 
				|| time_bits[1] != dt.getMinutes()
				|| time_bits[2] != dt.getSeconds()
			) throw new Error("couldn't parse time");

		} catch(e) {
			alert("your date is whack: " + e.message );
			return null;
		}

		return dt;
	}

	// zero pads a number, up to 2 digits
	function pad02d(num) {
		return ( num < 10 ? '0' + num : num );
	}


	// Options
	function init_option() {
		this.div = newNode('div');

		this.div.setAttribute('class','OneOption');

		if( this.header )
			this.div.innerHTML = "<h4>" + this.header + "</h4><br>";


		var para = newNode('p');

		if( this.setup_op ) this.setup_op(para);

		this.div.appendChild( para );

		return this.div;
	}
	
	function get_selected_ids() {
		var sel_ids = new Array();

		for(var i=0; i<unsafeWindow.ids.length; i++){
			if ( ! unsafeWindow.ids[i] ) continue;
			if( getNode('check_'+unsafeWindow.ids[i]).checked )
				sel_ids.push( unsafeWindow.ids[i] );
		}

		return sel_ids;
	}

	// Date option object

	
	// Add selected photos to Set or Group
	function SetOp() {

		this.header  = "Add <i>selected</i> photos to set";
		this.api     = true;
		this.mk_op   = init_option;

		this.set_list_failed = function(e) {
			var opts = this.select.options;
			while (this.select.length) {
				this.select.remove(0);
			}
			
			var o = newNode('option');
			o.text = 'failed to load photosets';
			opts[0] = o;
		}

		// the list of sets has loaded
		this.set_list_loaded = function(req, rsp) {
			var opts = this.select.options;
			while (this.select.length) {
				this.select.remove(0);
			}
			
			var o = newNode('option');
			var o1 = newNode('option');
			
			o1.text = '...choose a photoset...';
			o1.value = -31338;			
			o.text = '...create new set...';
			o.value = -31337;
			opts[0] = o1;
			opts[1] = o;
			
			
			var collection = document.evaluate( "//photoset", rsp, null, XPathResult.ANY_TYPE, null );

			var set = collection.iterateNext();

			while( set ) {
				var s       = new Object();

				s.title     = set.getElementsByTagName('title')[0].childNodes[0].nodeValue;
				s.node      = set;

				//var opt = new Option( s.title, null );
				var opt = newNode("option");				
				opt.text = s.title;
				opt.title = s.title;
				opt.value = s.node.getAttribute('id') + '|' + s.node.getAttribute('primary');
				opts[ opts.length ] = opt;

				set = collection.iterateNext();
			}
		}
		
		this.newset_submit = function(event) {
			event.preventDefault();
			event.returnValue = false;
			event.cancel = true;
			window.scrollTo(0,0);
			this.update_set();
			return false;
		}

		this.selchange = function(event) {
			var i = this.select.selectedIndex;
			var opts = this.select.options;
			if (opts[i].value == -31337) {
				var f = newNode('form');
				f.addEventListener('submit',getObjectMethodClosure1(this,'newset_submit'),true);								
				var t = newNode('input');
				t.type = 'text';
				t.id = 'new_set_name';
				t.size = 30;
				t.style.marginTop = '3px';
				f.appendChild(t);
				this.select.parentNode.insertBefore(f,this.select.nextSibling);
				t.focus();
			} else {
				var t = getNode('new_set_name');
				if (t) {
					t.parentNode.removeChild(t);
				}
			}
			event.preventDefault();
			return true;
		}
		
		
		this.new_set_added = function(req,rsp) {
			status_msg.hide();
			this.load_sets();
		}
		
		this.new_set_failed = function(err) {
			status_msg.msgbox('Failed to add to set: ' + err.msg);
			this.load_sets();
		}
		
		this.set_created = function(req,rsp) {
			var photoset = rsp.getElementsByTagName('photoset').item(0);
			var ids = get_selected_ids();
			var primary = ids[0];
			var id = photoset.getAttribute('id');
			
			GM_log('id: ' + id);
			GM_log('primary: ' + primary);
			
			status_msg.show('adding photos to set...');
			flickr_api_call('flickr.photosets.editPhotos',
											{ api_key: API_KEY, photoset_id: id, primary_photo_id: primary, 
											  photo_ids: ids.join(','), auth_token: TOKEN, http_method: 'POST' },
											getObjectMethodClosure2(this,'new_set_added'),
											getObjectMethodClosure1(this,'new_set_failed'));
		}
		
		this.set_create_failed = function(err) {
			status_msg.msgbox('couldn\'t create set: ' + err.msg);
		}
		
		this.set_photos_failed = function(e) {
			status_msg.msgbox('unable to edit photoset: ' + e.msg);
		}

		// the existing photos for the set have been loaded
		this.set_photos_loaded = function(req,rsp) {
			var setval        = this.selected_set;

			if( ! setval )
				throw new Error("selected_set not set");

			var collection = document.evaluate( "//photo", rsp, null, XPathResult.ANY_TYPE, null );
			var photo_ids  = new Array();

			var photo = collection.iterateNext();
			while( photo ) {
				photo_ids.push( photo.getAttribute('id') );
				photo = collection.iterateNext();
			}

			
			var set_id            = setval.split('|')[0];//set.node.getAttribute('id');
			var primary_photo_id  = setval.split('|')[1];//set.node.getAttribute('primary');
			var photo_ids_string  = photo_ids.join(",") + "," + this.selected_ids.join(",");

			flickr_api_call( "flickr.photosets.editPhotos", 
				{ api_key: API_KEY, photoset_id: set_id, primary_photo_id: primary_photo_id, photo_ids: photo_ids_string,
					 auth_token: TOKEN, http_method: 'POST' },
				getObjectMethodClosure2( this, 'new_set_added' ),
				getObjectMethodClosure1( this, 'new_set_failed') );
		}


		// "button" event, kicks off the update
		this.update_set = function() {
			var val = this.select.options[this.select.options.selectedIndex].value;
			if (val == -31338) {
				return;
			}
			
			this.selected_ids = get_selected_ids();
			
			if (val == -31337) {
				var t = getNode('new_set_name');
				var newset = t.value;
				var primary = this.selected_ids[0];
				
				status_msg.show('creating set...');
				flickr_api_call('flickr.photosets.create',
												{ api_key: API_KEY, title: newset, primary_photo_id: primary, 
												  auth_token: TOKEN, http_method: 'POST'},
												getObjectMethodClosure2(this,'set_created'),
												getObjectMethodClosure1(this,'set_create_failed'));
			}			
			
			else
			{			
				status_msg.show('adding photos to set...');
				var set = this.selected_set = this.select.options[ this.select.options.selectedIndex ].value;		
	
				if( ! this.selected_ids.length ) {
					alert("no photos selected");
					return;
				}
	
				var set_id            = set.split('|')[0];
	
				flickr_api_call( "flickr.photosets.getPhotos", 
												 { photoset_id: set_id }, 
												 getObjectMethodClosure2( this, 'set_photos_loaded' ),
												 getObjectMethodClosure1( this, 'set_photos_failed' ));
			}
			
			return false;
		}

		this.load_sets = function() {
			while (this.select.length) {
				this.select.remove(0);
			}
			var o = newNode('option');
			o.text = 'loading...';
			this.select.appendChild(o);

			flickr_api_call( "flickr.photosets.getList", {user_id: NSID}, 
											 getObjectMethodClosure2( this, 'set_list_loaded' ), 
											 getObjectMethodClosure1(this, 'set_list_failed'));
		}

		// create the interface
		this.setup_op = function(div) {

			var sel = this.select = newNode('select');
			sel.addEventListener('change',getObjectMethodClosure1(this,'selchange'),true);
			div.appendChild( sel );		
			
			div.appendChild( newNode('br') );

			div.appendChild( mk_a( 'add to set', getObjectMethodClosure(this,'update_set') ) );

			this.load_sets();
			
			return div;
		}

	} // end of SetOption




	// Add selected photos to group
	function GroupOp() {

		this.status         = new Object();
		this.update_status  = update_status;
		this.api            = true;
		this.mk_op          = init_option;

		this.header = "Add <i>selected</i> photos to group";		
		
		this.group_list_failed = function(e) {
			var opts = this.select.options;
			while (this.select.length) {
				this.select.remove(0);
			}
			
			var o = newNode('option');
			o.text = 'failed to load group list';
			opts[0] = o;
		}
		
		this.group_list_loaded = function(req,rsp) {
			var opts = this.select.options;
			while (this.select.length) {
				this.select.remove(0);
			}
			var collection = document.evaluate( "//group", rsp, null, XPathResult.ANY_TYPE, null );
			
			var o = newNode('option');
			o.text = '...select a group...';
			o.value = -31337;
			opts[0] = o;

			var group = collection.iterateNext();

			while( group ) {

				var name     = group.getAttribute("name");
				var id       = group.getAttribute("id");		
				

				//var opt = new Option( name, id );
				var opt = newNode("option");
				opt.text = name;
				opt.title = name;
				opt.value = id;
				opts[ opts.length ] = opt;

				group = collection.iterateNext();
			}
		}



		this.update_groups = function() {
			this.status["sent"]   = 0;
			this.status["failed"] = 0;

			var opts = this.select.options;
			var group_id = opts[ opts.selectedIndex ].value;
			if (group_id == -31337) {
				return;
			}

			var ids = get_selected_ids();
			
			this.status["total"] = ids.length;
			
			//status_msg.to_send = ids.length;
			

			if( ids.length > MAX_PICS_TO_GROUP 
				 && ! confirm("It is probably bad form to post more than " + MAX_PICS_TO_GROUP 
							+ " photos to a public group\nIf its a private group or you don't care, click OK. Otherwise click Cancel"
							+ "\n\nPlease - keep our Flickr beautiful!" ) 
				) return false;

			for( i in ids ) {
				flickr_api_call( "flickr.groups.pools.add", 
					{ api_key: API_KEY, auth_token: TOKEN, photo_id: ids[i], group_id: group_id },
					//{ group_id: group_id, photo_id: ids[i], email: USER_EMAIL, password: USER_PASSWORD, http_method: 'POST' }, 
					getObjectMethodClosure2( this, 'group_add_done'   ),
					getObjectMethodClosure1( this, 'group_add_failed' )
				);

				//this.status["sent"]++;
			}

			this.update_status();

			return false;
		}


		this.group_add_done = function(req,rsp) {
			//this.status["sent"]--;
			this.status["sent"]++;
			this.update_status();
		}
		this.group_add_failed = function(e) {
			//this.status["sent"]--;
			this.status["failed"]++;
			this.update_status();
		}

		this.load_groups = function()
		{		
			while (this.select.length) {
				this.select.remove(0);
			}
			var o = newNode('option');
			o.text = 'loading...';
			this.select.appendChild(o);
			
			flickr_api_call( "flickr.groups.pools.getGroups", 
											 { api_key: API_KEY, auth_token: TOKEN },
											 getObjectMethodClosure2( this, 'group_list_loaded' ),
											 getObjectMethodClosure1( this, 'group_list_failed' ) );
		}

		// create the interface
		this.setup_op = function(div) {

			this.select = newNode("select");

			div.appendChild( this.select );
			div.appendChild( newNode('br') );
			div.appendChild( mk_a( "add to group", getObjectMethodClosure( this, 'update_groups' ) ) );

			div.appendChild( newNode('br') );

			this.status_span = newNode('span');
			div.appendChild( this.status_span );	
			
			this.load_groups();

			return div;
		}
	} // end GroupOption



	


	function Selection() { }

	 //selection functions


	function fill_selection(event) {

		var filling = false;
		var first;
		var last;

		for( var i in unsafeWindow.ids ) {
			if( ! unsafeWindow.ids[i] ) continue;
			var check   = getNode('check_'+unsafeWindow.ids[i]);
			if( ! check.checked ) continue;

			if( ! first ) 
				first = check;
			last = check;
		}

		for( var i in unsafeWindow.ids ) {
			if( ! unsafeWindow.ids[i] ) continue;
			var check = getNode('check_'+unsafeWindow.ids[i]);

			if( check == first ) filling = true;

			if( filling  ) check.checked = true;

			if( check == last  ) filling = false;
		}
		event.preventDefault();
	}

	function invert_selection() {
		for( var i in unsafeWindow.ids ) {
			if( ! unsafeWindow.ids[i] ) continue;
			var check   = getNode('check_'+unsafeWindow.ids[i]);

			     if( ! check.checked )  check.checked = true;
			else if(   check.checked )  check.checked = false;

		}
	}


	function click_reload_batch_set() {
		var sel_ids = get_selected_ids();
		window.location = 'http://www.flickr.com/photos_batch_operations.gne?ids=' + sel_ids.join(',');
	}

	function mk_selection_div() {
		var sel_extra = newNode('div');
		
		var fill_button = mk_a('fill selection', fill_selection);		
		sel_extra.appendChild( fill_button );
		
		sel_extra.appendChild( newText(" | ") );
		
		var invert_sel_button = mk_a('invert selection', invert_selection);
		sel_extra.appendChild( invert_sel_button );

		sel_extra.appendChild( newText(" | reload batch with ") );

		var reload_batch_sel = mk_a('selection', click_reload_batch_set );
		sel_extra.appendChild( reload_batch_sel );
		/*
			 sel_extra.appendChild( newText(" | ") );

			 var reload_batch_set = mk_a('set', click_reload_batch_set );
			 sel_extra.appendChild( reload_batch_set );
		 */
		return sel_extra;
	}
	
	function script_init()
	{
		TOKEN = GM_getValue('auth');
		NSID  = GM_getValue('nsid');
		var sel_extra = mk_selection_div();
		
	
		// find the attachment spot for selection
		var sel_all = getNode('selectall');
		sel_all.parentNode.insertBefore( sel_extra, sel_all.nextSibling );
		
	
	
		var last_div;
	
		var divs = getTags('div');
		for( var i=0; i<divs.length; i++ ) {
			if( divs[i].getAttribute('class') == 'OneOption' ) last_div = divs[i];
		}
	
		var op_container = newNode('div');
	
		last_div.parentNode.appendChild(op_container);
		
	
	
		var op_cnt_closed = newNode('div');
		op_cnt_closed.appendChild( mk_a( '[ + show enhancements ]', function () {
			op_cnt_closed.style.display  = 'none';
			op_cnt_open.style.display    = 'block';
			return false;
		} ) );
	
		var op_cnt_open = newNode('div');	
		op_cnt_open.style.display = 'block';		
		op_cnt_closed.style.display = 'none';
			
		op_container.appendChild(op_cnt_closed);
		op_container.appendChild(op_cnt_open);	
	
		// Add the classes which provide extra operations on the RHS
		var ops = [
			GroupOp,
			SetOp
		];	
	
		for( var j=0; j<ops.length; j++ ) {
			var option  = new ops[j]();
			var new_div = option.mk_op();
	
			if( ! new_div ) continue;
			op_cnt_open.appendChild( new_div );
		}
	}
	
	this.token_loaded = function(req,rsp) {		
		status_msg.hide();
		var token = rsp.getElementsByTagName('token')[0].firstChild.nodeValue;
		var user = rsp.getElementsByTagName('user')[0];
		var nsid = user.getAttribute('nsid');		
		
		GM_setValue('auth',token);
		GM_setValue('nsid',nsid);
		script_init();		
	}
	
	this.token_failed = function(e) {
		status_msg.msgbox('Couldn\'t authorize, for whatever reason.');
	}
	
	// set it all up
	
	this.getToken = function(str)
	{
		var token = str;
		if (!/\d{3}-\d{3}-\d{3}/.test(token)) {
			alert('please enter a valid token');
			var e = getNode('woowoo');
			e.focus();
			e.select();
			return false;
		} else {
			status_msg.show('authorizing...');
			flickr_api_call("flickr.auth.getFullToken",{api_key: API_KEY, mini_token: token}, getObjectMethodClosure2(this,'token_loaded'),getObjectMethodClosure1(this,'token_failed'));
		}
	}	
	
	if (!GM_getValue('auth')) {
		status_msg.prompt("This script needs to be authorized. <br>" +
		      "Click [<a onclick='window.open(\"http://flickr.com/auth-12707\",\"\",\"menubar=no,location=no,resizable=yes,scrollbars=yes,width=600,height=500\"); return false'>here</a>], " +
					"follow the instructions in the popup window,<br> " +
					"then return here and paste in the minitoken you receive.<br> " +
					"Popup blockers may cause this not to work.<br>You'll only have to do this once.",getObjectMethodClosure1(this,'getToken'));
		//status_msg.prompt('enter your jobbie',function(str) {alert(str);});
		//var w = window.open("http://flickr.com/auth-12707",'','menubar=no,location=no,resizable=yes,scrollbars=yes,width=600,height=500');		
	}	
	else {		
		script_init();
	}
	
})();

// vim: se ft=javascript:
