// The Rye's KoL Area Trainer
//
// Trains your familiar in the area based on values from http://www.therye.org/familiars/
//
// written by bigfreak on commision from The Rye
//
// latest version at: http://www.therye.org/gp.php/familiars/therye_arena.user.js

// ==UserScript==
// @name           The Rye's KoL Area Trainer
// @namespace      http://rghware.com
// @include        *kingdomofloathing.com/arena.ph*
// @include        *127.0.0.1:*/arena.ph*
// @description    Version 0.0.0
// ==/UserScript==

/*
 trainer - main class to control all others
 */

// trainer() runs first ( in the context of Greasemonkey's anonymous function )
function trainer()
{	var me = this;

	// init	
	me.topWnd = window.top || window;
	if ( me.topWnd.wrappedJSObject )
		me.topWnd = me.topWnd.wrappedJSObject;
	me.doc = document.wrappedJSObject || document;
	me.GM_getValue = GM_getValue;
	me.GM_setValue = GM_setValue;
	me.GM_xmlhttpRequest = GM_xmlhttpRequest;
	me.userid = me.parse( 
		me.topWnd.document.getElementsByName( 'charpane' )[0].contentDocument.body.innerHTML,
		'href="charsheet.php"><b>', '</b></a>' ) || me.parse( 
		me.topWnd.document.getElementsByName( 'charpane' )[0].contentDocument.body.innerHTML,
		'href="charsheet.php">', '</a>' );
	me.loadingData = false;
	me.funcs = [];
	me.events = [ 'Ultimate Cage Match', 'Scavenger Hunt', 'Obstacle Course', 'Hide and Seek' ];

	// star images
	me.starImgs = [
		'data:image/gif;base64,'+
    'R0lGODlhPAAUAOYAAM4AANbR0drIyM/Pz9DQ0M0AANbS0tliYtLS0tEHB9LR0dEICNhcXNQeHt22'+
    'tt21td2BgdTS0tQiItQjI9QdHdyKitvHx92jo9bQ0NQfH9HQ0NHR0dTT09IYGNAKCty2ts6zs9yD'+
    'g9qMjNu0tNXS0tINDdhUVNABAdySktXR0dUfH9dTU9IXF9lUVN2AgM8BAc3NzdMeHs8DA9IeHtpr'+
    'a9AJCdMcHNrKytUjI9lnZ92Ojt2lpdAFBdUsLN20tNCystYlJdUgIM8EBNhZWdvIyNdGRtUxMdDH'+
    'x9x+ftUlJdnQ0Nyjo9phYc+2ttMnJ9lNTdPS0tAEBNyAgNgqKtyMjM8CAt2CgtnNzdlPT9zBwdrJ'+
    'ydfQ0N1pad2urttra9yJic8AANAAAMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5'+
    'BAAAAAAALAAAAAA8ABQAAAf/gGKCg4SFhoeIiYqLjI2Oj5CCPy1DDCYiEYgjKwwMTyCRoYRHXFUL'+
    'CSUXBIYWQAkLL1NNorRiAUwFYWFgqoREGWBhABQCtbUGBwC7MkurYhYSwWATxcbHOcpgQjsEAhTK'+
    'ACrV1scHuWAnIUngGePktQHJumDSE1rv7yQ0wfM9N/jvBnTht8vLBoDWBjxYME9XGAgaENJSWENa'+
    'EGkAICiQGGmAAw/SsCjRkQ2MlY0cG3mMEqxAkS1iEFRQtkvKwZSKBnxIEAyAhCuDoHyhCcBFRJyH'+
    'CPjgAYAeDncxZ9IrgAQlUkJZnHRoEMMIVEEIqMxoYIMFiquFImAIwNYAIhgcCFKwXYu2rqJAADs=',
		'data:image/gif;base64,'+
    'R0lGODlhPAAUANUAAOvr6/39/c3NzdfX19DQ0O/v7/n5+dPT0+Dg4PLy8uHh4efn59/f3/z8/NbW'+
    '1s7OzvT09N7e3ujo6Pr6+tjY2NHR0fHx8d3d3dXV1erq6tTU1Onp6ePj49ra2uLi4ubm5vv7+9nZ'+
    '2ezs7P///8zMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAA8ABQAAAaWQJJw'+
    'SCwajyQBcslsOpeLp3RKHQQI1KzWmBlFtuDsozEqhM9PxmgUeKDfyMR6dIGjOYC8fj5KIP4DDg4H'+
    'dlMEFnyJihCEhVICEoqSH0qOVAwBkmsTIZZbAyCSBVieWgIGkqSlWRSaX6tZG3MBmWsisFmofRoH'+
    'EGsGuFMDIw0KQwILaxjBTxIFFUYdE8bMTQNLBB7V20xBADs=',
		'data:image/gif;base64,'+
    'R0lGODlhPAAUANUAAOvr6/39/c3NzdfX19DQ0O/v7/n5+dPT0+Dg4PLy8uHh4efn59/f3/z8/NbW'+
    '1s7OzvT09N7e3ujo6Pr6+tjY2NHR0fHx8d3d3dXV1erq6tTU1Onp6ePj49ra2uLi4ubm5vv7+9nZ'+
    '2ezs7P///8zMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAA8ABQAAAbkQJJw'+
    'SCwSBcakEalsOp+LpzIqrUoHAYJ1iNVuv8XMKAIWk8HoR2NU+KrZaDRjNAo8tvP6Pb5N0EcXfX+B'+
    'fEkcAIiJfyMJCI4DDg4HJIeJiIuNj5GTfAQWi6ChEJOeoaZ0o4UkAhKnoB9Mq62uf7CqQwwBrhMh'+
    'Sbm7vbdEAyCmBV5JxMbIwkICBqbMS9Ch0s0Up2dK2KbazUIbfwG6dCJO4XTjf+bfQ9QJGgcQdAZO'+
    '7/HzI/XtJAMjDQqGCFhAB4MSfwAFEhxhsJ2EAhWMdJgQMMnDiEUmVvw2oAkBDwc9guRHEkwQADs=',
		'data:image/gif;base64,'+
    'R0lGODlhPAAUANUAAOvr6/39/c3NzdfX19DQ0O/v7/n5+dPT0+Dg4PLy8uHh4efn59/f3/z8/NbW'+
    '1s7OzvT09N7e3ujo6Pr6+tjY2NHR0fHx8d3d3dXV1erq6tTU1Onp6ePj49ra2uLi4ubm5vv7+9nZ'+
    '2ezs7P///8zMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAA8ABQAAAb/QJJw'+
    'OBQQj0iiMcksNp+LZzMqTVKrxEGAgM1uu0MtF0zKjCLk8jltRoMfjVGBDJfT43MwYzQKPPR8foB9'+
    'f10JfCMXYId8ioaIjkgcAJSViCMJCJoDDg4HJJOVlJeZm52foaKkmgicniQEFpeztBCfsbS5fLaw'+
    'srq1nyQCEr+XH0vCxMV8x0XKy81EDAG/EyFJ09XXSNm61kwDILkFY0nh4+VI57TkTQIGuelI7/FP'+
    '9LTyRxS6bkz7uf2S/KMVEMkGRAGo8RHx5GAghSMYNnHYB6JEJvAwaTgAgY+BJxkTbOw44mOTkCM9'+
    'NhkwooGCIgv4YADX8qUQATFHzDRXE6ZMJSYSClRA0mGCTSRBhx4pevRIUqJGwTUh4EEqE6pWk2BN'+
    'w7XrkyAAOw==' ];
    
	// create worker objects
	me.fams = new familiars( me );
	me.arena = new arena( me );

	// add new gui elements to the arena
	me.injectGUI();

	// do the download of familiar strengths data, if appropriate
	if ( (new Date()).getDate() != GM_getValue( 'lastUpdate', 0 ) )
	{
		// Tell the familiars object to cache the latest chart.
		me.fams.httpUpdateStrengths();
	}

	me.pickMatch();
 	me.updateGUI();
	
	// note: we're now going to exit Greasemonkey's anonymous function, but an instance of this object
	// will be attached to the document object so this object and all of it's child objects will
	// persist outside the context of the anonymous function.  This code will therefore live long after
	// Greasemonkey has tried to flush it down the crapper... Excellect!  *air guitars*
};

// lib: gv() sets per-user settings
trainer.prototype.gv = function( varName, defVal )
{	var me = this;
	var ret = me.GM_getValue( me.userid + '_' + varName, defVal );
	return ( ret == '' ) ? defVal : ret;
};

// lib: sv() loads per-user setting
trainer.prototype.sv = function( varName, value )
{	var me = this;
	me.GM_setValue( me.userid + '_' + varName, value );
};

// lib: gSet() sets global varibles which will persist through this page loads
trainer.prototype.gSet = function( varName, value )
{	this.topWnd['trainer' + varName] = value;
};

// lib: gGet() gets global varibles which may have been set before this page load
trainer.prototype.gGet = function( varName, defVal )
{	
	var ret = this.topWnd['trainer' + varName] || defVal;
	return ( ret == '' ) ? defVal : ret;
};

// lib: parse() searches the source string and splits out data found beteem strLeft and strRight
trainer.prototype.parse = function( strSrc, strLeft, strRight )
{
	var leftPos = strSrc.indexOf( strLeft );
	if ( leftPos == -1 )
		return '';
	var rightPos = strSrc.indexOf( strRight, leftPos + strLeft.length );
	if ( rightPos == -1 )
		rightPos = strSrc.length;
	return strSrc.substring( leftPos + strLeft.length, rightPos );
};

// injectGUI() terraforms the Arena
trainer.prototype.injectGUI = function()
{	var me = this;

	// get rid of Susie and the welcome wagon
	me.doc.getElementsByTagName( 'table' )[2].innerHTML = '';
	
	// put events all on one line
	var objs = me.doc.getElementsByName( 'event' );
	for ( var i = 0; i < objs.length - 1; ++i )
		objs[i].parentNode.removeChild( objs[i].nextSibling.nextSibling );
	
	// make "Only 1 win left until your next prize!" stand out
	var objs = me.doc.getElementsByTagName( 'p' );
	for ( var i = 0; i < objs.length && objs[i].innerHTML.indexOf( 'Only 1 win left until your next prize!' ) < -1; ++i );
	if ( i < objs.length )
		objs[i].innerHTML = objs[i].innerHTML.replace( 'Only 1 win left until your next prize!',
			'<font color="red"><b>Only 1 win left until your next prize!</b></font>' );
	
	// find the injection site (the 'Compete! (1)' button)
	var objs = me.doc.getElementsByTagName( 'input' );
	for ( var i = 0; i < objs.length && objs[i].type != 'submit'; ++i );
	if ( i >= objs.length )
		return me.msgCallback( 'Unable to inject The Rye\'s KoL Area Trainer.  Server update?  Conflicting script?' );
	var node = objs[i];
	var endNode = node.parentNode.nextSibling;
	
	// take control of the compete button
	node.id = 'compete';
	node.type = 'button';
	node.onclick = function() { me.compete(); };

	// advanced features
	var obj = me.doc.createElement( 'p' );
	obj.innerHTML = '' +
		'[ <a href="javascript:;" id="showchart">pick manually</a> ] ' +
		'[ <a href="javascript:;" id="autotrain">auto compete</a> ] ' +
		'[ <a href="javascript:;" id="schedule">training schedule</a> ]';

	// inject the advanced features section
	node.parentNode.parentNode.insertBefore( obj, endNode );

	// connect code to advanced features links
	me.doc.getElementById( 'showchart' ).onclick = function() { me.showChart(); };
	me.doc.getElementById( 'autotrain' ).onclick = function() { me.autoTrain( prompt( 'How many turns?' ) ); };
	me.doc.getElementById( 'schedule' ).onclick = function() { me.showSchedule(); };

	// odds section
	var obj = me.doc.createElement( 'p' );
	obj.innerHTML = '' +
		'<b>Odds:</b>' +
		"<table border='0' cellpadding='0' cellspacing='1' width='80%' bgcolor='#000000' style='font-family: Arial; font-size: 11px;'><tr>" + 
		"<td id='lose' bgcolor='#F73842' width='20%' align='center' style='display: none;'>Lose<br />20%</td>" +
		"<td id='five' bgcolor='#00A469' width='20%' align='center' style='display: none;'>5xp<br />20%</td>" +
		"<td id='four' bgcolor='#0000FF' width='20%' align='center' style='display: none; color: #bbbbbb;'>4xp<br />20%</td>" +
		"<td id='three' bgcolor='#F6D700' width='20%' align='center' style='display: none;'>3xp<br />36%</td>" +
		"<td id='two' bgcolor='#FF9900' width='20%' align='center' style='display: none;'>2xp<br />20%</td>" +
		"</tr></table>";
	
	// inject the odds section
	node.parentNode.parentNode.insertBefore( obj, endNode );

	// main interface
	var obj = me.doc.createElement( 'p' );
	obj.innerHTML = '' +
		'<b>The Rye\'s Arena Trainer:</b><br />' +
		"<input id='pickmatch' type='button' class='button' disabled value='Pick a match' /> " +
		"where the chance of getting 5 experience is " +
		"<input id='fivexp' type='text' class='text' disabled value='' size='2' maxsize='2' style='width: 25px; text-align: right;' />" +
		"% or better and the chance of losing is " + 
		"<input id='risk' type='text' class='text' disabled value='' size='2' maxsize='2'  style='width: 25px; text-align: right;' />% or less." +
		"<div id='chartdisp' style='background-color: #ffffff; padding: 10px; border: thin black solid; display: none; font-family: Arial; font-size: 11px; border: 1px solid black; overflow-y: auto; overflow-x: hidden;'></div>";

	// inject main interface
	node.parentNode.parentNode.insertBefore( obj, endNode );

	// link interface with code
	me.doc.getElementById( 'pickmatch' ).onclick = function() { me.pickMatch(); };
	me.doc.getElementById( 'fivexp' ).onchange = function() { me.sv( this.id, this.value ); me.updateGUI(); };
	me.doc.getElementById( 'risk' ).onchange = function() { me.sv( this.id, this.value ); me.updateGUI(); };
	
	// results display
	var obj = document.createElement( 'p' );
	obj.innerHTML = '' +
		'<b>Results:</b>' +
		"<div id='dispresults' style='font-family: Arial; font-size: 11px; width: 400px; height: 75px; border: 1px solid black; overflow-y: scroll; overflow-x: hidden;'>" +
		"</div>";

	// inject the results display after compete button
	node.parentNode.parentNode.insertBefore( obj, endNode );

	// change font and font size
	var objs = me.doc.getElementsByTagName( '*' );
	for ( var i = 0; i < objs.length; ++i )
		if ( objs[i].style )
		{
			objs[i].style.fontFamily = 'Arial';
			objs[i].style.fontSize = '11px';
		}

	// cause the form controls to update the odds
	var objs = me.doc.getElementsByTagName( 'input' );
	for ( var i = 0; i < objs.length; ++i )
		if ( objs[i].type == 'radio' )
		{
			objs[i].watch( 'checked', 
				function( prop, oldval, newval )
				{
					me[this.name] = this.value;
					setTimeout( function() { me.detectTards(); }, 100 );
					return newval;
				} );
			objs[i].addEventListener( 'change', function() { me[this.name] = this.value; me.updateGUI(); }, false );
			if ( objs[i].checked )
				me[objs[i].name] = objs[i].value;
		}
};

// detectTards() looks for the green button and repicks the match
trainer.prototype.detectTards = function()
{	var me = this;
	if ( me.doc.forms['compete'].elements[10].style.background != '' )
	{
		me.doc.forms['compete'].elements[10].style.background = '';
		me.processPending(
			function()
			{
				me.msgCallback( 'Detected Tard\'s Cake-Shaped Arena Script.' );
				var opp = me.fams.getOpponent( me.whichopp );
				var odds = me.arena.runFight( me.fams.mybuddy(), opp, me.event - 1 );
				me.msgCallback( 'Tard\'s picked: ' + opp.name + ' in ' + me.events[me.event-1] + 
					' lose: ' + odds.lose + '% 5xp: ' + odds.five + '%' );
				me.msgCallback( 'Re-picking match...' );
				me.pickMatch();
			} );
	}
};

// processPending() puts functions in a list and executes them once the userid has been parsed
trainer.prototype.processPending = function( func )
{	var me = this;
	// if a function was pass add it to the queue
	if ( func )
		me.funcs[me.funcs.length] = func;
	// if we're waiting on the external web page or if we don't have any functions to run, exit
	if ( me.loadingData || me.funcs.length == 0 )
		return;
	// run all the functions in the queue
	for ( var i = 0; i < me.funcs.length; ++i )
		me.funcs[i]();
	// purge the function queue
	me.funcs.splice( 0, me.funcs.length );
};

// updateGUI() updates the user interface with a new best match based on user preferences
trainer.prototype.updateGUI = function()
{	var me = this;
	
	// if we're loading data from the familiar chart, queue this function to run when it's time
	if ( me.loadingData )
		return me.processPending( function() { me.updateGUI(); } );
	
	// load the user's preferences
	me.doc.getElementById( 'fivexp' ).value = me.gv( 'fivexp', 80 );
	me.doc.getElementById( 'risk' ).value = me.gv( 'risk', 15 );
	
	// enable the user interface
	me.doc.getElementById( 'fivexp' ).disabled = me.doc.getElementById( 'risk' ).disabled = 
		me.doc.getElementById( 'pickmatch' ).disabled = me.doc.getElementById( 'compete' ).disabled = false;

	// if nothing is selected show no odds
	if ( me.event == 0 && me.whichopp == 0 )
	{
		me.doc.getElementById( 'lose' ).style.display = 'none';
		me.doc.getElementById( 'five' ).style.display = 'none';
		me.doc.getElementById( 'four' ).style.display = 'none';
		me.doc.getElementById( 'three' ).style.display = 'none';
		me.doc.getElementById( 'two' ).style.display = 'none';
		return;
	}

	// show odds on current fight
	var odds = me.arena.runFight( me.fams.mybuddy(), me.fams.getOpponent( me.whichopp ), me.event - 1 );
	for ( var key in odds )
	{
		var obj = me.doc.getElementById( key );
		obj.style.width = odds[key] + '%';
		obj.style.display = odds[key] > 0 ? '' : 'none';
		var msg = '';
		switch ( key )
		{
			case 'lose':
				msg = 'lose';
			break;
			case 'five':
				msg = '5xp';
			break;
			case 'four':
				msg = '4xp';
			break;
			case 'three':
				msg = '3xp';
			break;
			case 'two':
				msg = '2xp';
			break;
		}
		obj.innerHTML = msg + '<br />' + odds[key] + '%';
	}
};

// pickMatch() shows the user the best match based on his preferences
trainer.prototype.pickMatch = function()
{	var me = this;

	// if we're loading data from the familiar chart, queue this function to run when it's time
	if ( me.loadingData )
		return me.processPending( function() { me.pickMatch(); } );

	// find the best matches given the criteria
	me.msgCallback( 'Examining opponent strengths and the strengths of your familiar...' );
	var matches = me.arena.runFights( me.fams.mybuddy(), me.fams.getOpponents(), me.gv( 'risk', 15 ), me.gv( 'fivexp', 80 ) );
	// if no good matches exist, bail
	if ( matches.length == 0 )
	{
		// deselect all the buttons and tell the user no good matches exist
		var objs = me.doc.getElementsByTagName( 'input' );
		for ( var i = 0; i < objs.length; ++i )
			if ( objs[i].type == 'radio' )
				objs[i].checked = false;
		me.event = 0;
		me.whichopp = 0;
		me.msgCallback( 'No good matches exist at this weight, under these restrictions.', 'red' );
		me.updateGUI();
		return;
	}

	me.msgCallback( 'Acceptable match found: ' + matches[0].enemy.weight + 'lb ' + matches[0].enemy.name + ' in ' + me.events[matches[0].bestEvent], 'blue' );

	// select the event button
	me.doc.getElementsByName( 'event' )[matches[0].bestEvent].checked = true;
	// select the opponent button
	var objs = me.doc.getElementsByName( 'whichopp' );
	for ( var i = 0; i < objs.length && objs[i].value != matches[0].enemy.whichopp; ++i );
	if ( i < objs.length )
		objs[i].checked = true;
	me.updateGUI();
};

// compete() runs the selected match in the arena
trainer.prototype.compete = function()
{	var me = this;
	me.executeMatch( me.whichopp, me.event );
};

// executeMatch() sends arena.php request to servers to process a round of combat 
trainer.prototype.executeMatch = function( whichopp, event )
{	var me = this;

	// disable user interface while we have the server run the match (re-enabled in updateGUI after charpane refresh)
	me.doc.getElementById( 'fivexp' ).disabled = me.doc.getElementById( 'risk' ).disabled = 
		me.doc.getElementById( 'pickmatch' ).disabled = me.doc.getElementById( 'compete' ).disabled = false;
	
	// have the KoL server run the arena match
	GM_xmlhttpRequest(
		{
	    method: 'POST',
	    url: me.doc.location.href,
	    headers: { 'User-agent': 'Mozilla/4.0 (compatible) Greasemonkey', 'Accept': 'text/html', 
	    	'Content-type': 'application/x-www-form-urlencoded' },
	    data: 'action=go&whichopp=' + whichopp + '&event=' + event,
	    onload: function( responseDetails )
	    {
	    	// bail out if the server pukes
	    	if ( responseDetails.status != 200 )
	    		return;
	    	
	    	// tell the user what happened
	    	var html = me.doc.createElement( 'html' );
	    	html.innerHTML = responseDetails.responseText;
	    	
	    	var text = html.getElementsByTagName( 'table' )[0].innerHTML.replace( /\<.+?\>/g, ' ' );
	    	
	    	var xps = { 5: ['#00A469', '#000000'], 4: ['#0000FF', '#bbbbbb'], 3: ['#F6D700', '#000000'], 
	    		2: ['#FF9900', '#000000'] };
	    	
	    	for ( var i in xps )
	    		text = text.replace( 'gains ' + i + ' experience', 
	    			'<span style="color: ' + xps[i][1] + '; background-color: ' + xps[i][0] + ';">' + 'gains <b>' + i + '</b> experience' + '</span>' );
	    	
	    	text = text.replace( 'gains a pound!', '<b>gains a pound!</b>' ).replace( 
	    		'You acquire an item', '<font color="blue">You acquire an item</font>' ).replace( 
	    		'Only 1 more win', '<font color="red">Only 1 more win</font>' ).replace( 'Only ', 'Only <b>' ).replace( 
	    		' more win', '</b> more win' ).replace( 'lost.', '<span style="background-color: F73842;">lost.</span> 8-(' );
	    	
	    	me.msgCallback( text );
	    	
	    	// reload the charpane to process gained poundage.  This also shows the user updated Adv and meat.
	  		var obj = me.topWnd.document.getElementsByName( 'charpane' )[0];
	  		obj.contentWindow.location = obj.contentWindow.location.href;
	    }
		} );
};

// msgCallback() is used internally to show the user what's going on.
trainer.prototype.msgCallback = function( message, color )
{	var me = this;
	var obj = me.doc.getElementById( 'dispresults' );
	if ( !obj )
		return alert( message );
	obj.innerHTML += '<span style="color:' + ( color || 'black' ) + ';">' + message + "</span><br />";
	obj.scrollTop += 600;
};

// showChart() brings up a chart which shows details outputs for the current arena round
trainer.prototype.showChart = function()
{	var me = this;

	var disp = '<table border="0" cellpadding="2" cellspacing="2" style="font-family: Arial; font-size: 11px; text-align: center;">';
	disp += '<tr><td colspan="2" style="width: 70px; background-color: #FF9900;">Opponent</td><td style="width: 80px; background-color: #FF9900;">' +
		'Ultimate Cage Match</td><td style="width: 80px; background-color: #FF9900;">Scavenger Hunt</td>' +
		'<td style="width: 80px; background-color: #FF9900;">Obstacle Course</td><td style="width: 80px; background-color: #FF9900;">Hide and Seek</td>';

	var curOpp = '';
	var matches = me.arena.runFights( me.fams.mybuddy(), me.fams.getOpponents(), me.gv( 'risk', 15 ), me.gv( 'fivexp', 80 ), 
		function( mybuddy, enemy, odds, event )
		{
			if ( curOpp != enemy.name + enemy.weight )
			{
				curOpp = enemy.name + enemy.weight;
				disp += '</tr><tr><td style="width: 35px; background-color: #CCCCCC;"><img src="http://images.kingdomofloathing.com/itemimages/' + enemy.img + '" /></td>';
				disp += '<td style="width: 35px; background-color: #CCCCCC;">' + enemy.weight + ' lbs</td>';
			}
			
			disp += '<td id="' + enemy.whichopp + 'x' + event + '" style="border: 1px #CCCCCC solid; width: 80px; background-color: #CCCCCC;" onmouseover="this.style.border=\'1px #00A469 solid\';" onmouseout="this.style.border=\'1px #CCCCCC solid\';" onclick="document.trainer.choseMatch( this.id );">';
				disp += '<img src="' + me.starImgs[enemy.stars[event]] + '" />';
			disp += '<br />' +
				"<table style='font-family: Arial; font-size: 11px;' border='0' cellpadding='0' cellspacing='0' width='100%' title='" + 
				( odds.lose > 0 ? 'Lose: ' + odds.lose + '% ' : '' ) +
				( odds.five > 0 ? '5xp: ' + odds.five + '% ' : '' ) +
				( odds.four > 0 ? '4xp: ' + odds.four + '% ' : '' ) +
				( odds.three > 0 ? '3xp: ' + odds.three + '% ' : '' ) +
				( odds.two > 0 ? '2xp: ' + odds.two + '% ' : '' ) + "'><tr>" +
				( odds.lose > 0 ? "<td bgcolor='#F73842' width='" + odds.lose + "%'>&nbsp;</td>" : '' ) +
				( odds.five > 0 ? "<td bgcolor='#00A469' width='" + odds.five + "%'>&nbsp;</td>" : '' ) +
				( odds.four > 0 ? "<td bgcolor='#0000FF' width='" + odds.four + "%'>&nbsp;</td>" : '' ) +
				( odds.three > 0 ? "<td bgcolor='#F6D700' width='" + odds.three + "%'>&nbsp;</td>" : '' ) +
				( odds.two > 0 ? "<td bgcolor='#FF9900' width='" + odds.two + "%'>&nbsp;</td>" : '' ) +
				"</tr></table>";
			disp += '</td>';
		} );
	disp += '</tr></table>';

	var div = me.chartDiv();
	div.style.height = '';
	div.innerHTML = disp;
	div.style.display = '';
};


// choseMatch() gets called when the user picks a match from the chart
trainer.prototype.choseMatch = function( id )
{	var me = this;
	me.chartDiv().style.display = 'none';
	
	var event = Number( id.split('x')[1] );
	var whichopp = Number( id.split('x')[0] );
	
	// select the event button
	me.doc.getElementsByName( 'event' )[event].checked = true;
	// select the opponent button
	var objs = me.doc.getElementsByName( 'whichopp' );
	for ( var i = 0; i < objs.length && objs[i].value != whichopp; ++i );
	if ( i < objs.length )
		objs[i].checked = true;
	me.updateGUI();
};

// chartDiv() returns a div used for displaying charts
trainer.prototype.chartDiv = function()
{	var me = this;
	var ret = me.doc.getElementById( 'chartdisp' );
	if ( ret.style.position != '' )
		return ret;
	ret.style.position = 'absolute';
	var topObj = me.doc.getElementsByName( 'whichopp' )[0].parentNode.parentNode.parentNode.parentNode.previousSibling;
	var leftObj = me.doc.getElementsByName( 'event' )[0].parentNode.parentNode.parentNode.parentNode;
	ret.style.top = ( me.totalOffset( topObj, 'offsetTop' ) + 20 )+ 'px';
	ret.style.left = ( me.totalOffset( leftObj, 'offsetLeft' ) - 10 ) + 'px';
	ret.style.width = ( leftObj.offsetWidth + 20 ) + 'px';
	return ret;
};

trainer.prototype.totalOffset = function( obj, prop )
{
	var total = obj[prop];
	for ( obj = obj; obj.offsetParent; obj = obj.offsetParent )
		total += obj.offsetParent[prop];
	return total;
}

trainer.prototype.showSchedule = function()
{	var me = this;

	var disp = '<table border="0" cellpadding="2" cellspacing="2" style="font-family: Arial; font-size: 11px; text-align: center;">';

	var mybuddy = me.fams.mybuddy();
	var opponents = me.fams.getOpponents();

	for ( var i = 1; i < 50; ++i )
	{
		if ( i % 10 == 1 )
			disp += '<tr><td colspan="2" style="width: 70px; background-color: #FF9900;">Your Pet</td><td colspan="2" style="width: 70px; background-color: #FF9900;">Opponent</td>' +
				'<td style="width: 80px; background-color: #FF9900;">Event</td><td style="width: 80px; background-color: #FF9900;">Odds</td><td style="background-color: #FFFFFF;"><a href="javascript:;" onclick="javascript:document.getElementById(\'chartdisp\').style.display=\'none\';">Close</a></td></tr>';
		mybuddy.weight = i;
		var matches = me.arena.runFights( mybuddy, opponents, me.gv( 'risk', 15 ), me.gv( 'fivexp', 80 ) );

		disp += '<tr>';
		disp += '<td width="30px"><img src="http://images.kingdomofloathing.com/itemimages/' + mybuddy.img + '" title="' + mybuddy.name + '" /></td>';
		disp += '<td width="45px"><b>' + i + '</b> lbs</td>';

		if ( !matches.length )
		{
			disp += '<td colspan="4">No good matches exist under these restrictions.</td></tr>';
			continue;
		}

		var m = matches[0];
		disp += '<td width="30px"><img src="http://images.kingdomofloathing.com/itemimages/' + m.enemy.img + '" title="' + m.enemy.name + '" /></td>';
		disp += '<td width="45px"><b>' + m.enemy.weight + '</b> lbs</td>';

		disp += '<td width="100">' + me.events[m.bestEvent] + '</td>';
		
		disp += '<td width="100">';		
		var title = 'Lose: ' + m.odds.lose + '% 5xp: ' + m.odds.five + '% 4xp: ' + 
			m.odds.four + '% 3xp: ' + m.odds.three + '% 2xp: ' + m.odds.two + '%';
		disp += "<table style='font-family: Arial; font-size: 11px;' border='0' cellpadding='0' cellspacing='0' width='100%' title='" + 
			( m.odds.lose > 0 ? 'Lose: ' + m.odds.lose + '% ' : '' ) +
			( m.odds.five > 0 ? '5xp: ' + m.odds.five + '% ' : '' ) +
			( m.odds.four > 0 ? '4xp: ' + m.odds.four + '% ' : '' ) +
			( m.odds.three > 0 ? '3xp: ' + m.odds.three + '% ' : '' ) +
			( m.odds.two > 0 ? '2xp: ' + m.odds.two + '% ' : '' ) + "'><td>" +
			( m.odds.lose > 0 ? "<td style='background-color: #F73842' width='" + m.odds.lose + "%'>&nbsp;</th>" : '' ) +
			( m.odds.five > 0 ? "<td style='background-color: #00A469' width='" + m.odds.five + "%'>&nbsp;</th>" : '' ) +
			( m.odds.four > 0 ? "<td style='background-color: #0000FF' width='" + m.odds.four + "%'>&nbsp;</th>" : '' ) +
			( m.odds.three > 0 ? "<td style='background-color: #F6D700' width='" + m.odds.three + "%'>&nbsp;</th>" : '' ) +
			( m.odds.two > 0 ? "<td style='background-color: #FF9900' width='" + m.odds.two + "%'>&nbsp;</th>" : '' ) +
			"</td></table>";
		disp += '</td>';

		disp += '</tr>';
	}

	disp += '</table>';

	var div = me.chartDiv();
	div.style.height = '300px';
	div.innerHTML = disp;
	
	var objs = div.getElementsByTagName( 'td' );
	for ( var i = 0; i < objs.length; ++i )
		if ( !objs[i].style.backgroundColor && !objs[i].bgcolor )
			objs[i].style.backgroundColor = '#CCCCCC';
	
	div.style.display = '';
};

// autoTrain() will automatcially run fights for x amount of turns
trainer.prototype.autoTrain = function( turns )
{	var me = this;
	if ( !turns && !me.atStartAdv && !me.atTurns )
		return;	// user canceled, bail out

	var strHTML = me.topWnd.document.getElementsByName( 'charpane' )[0].contentDocument.body.innerHTML;
	var adv = Number( me.parse( strHTML, 'Adv</a>:</td><td align="left"><b>', '</b>' ) || 
		me.parse( strHTML, 'doc("adventures");\'><br><span class="black">', '</span>' ) );
	var meat = Number( ( me.parse( strHTML, 'Meat:</td><td align="left"><b>', '</b>' ) || 
		me.parse( strHTML, 'doc("meat");\'><br><span class="black">', '</span>' ) ).replace( /\,/g, '' ) );
	
	if ( !me.atStartAdv && !me.atTurns )
	{
		me.atStartAdv = adv;
		me.atTurns = turns;
	}
	else
		if ( me.atStartAdv - adv == me.atTurns )
			return me.stopAutoTrain( 'Done training for ' + me.atTurns + ' adventures.' );
	if ( adv < 1 )
		return me.stopAutoTrain( 'You don\'t have enough adventures to continue training.' );
	if ( meat < 100 )
		return me.stopAutoTrain( 'You don\'t have 100 meat to continue training.' );

	me.pickMatch();

	if ( me.event == 0 && me.whichopp == 0 )
		return me.stopAutoTrain( 'Training halted.' );

	me.msgCallback( 'Competing in the arena... (' + (me.atStartAdv - adv + 1) + ' of ' + me.atTurns + ')' );

	me.compete();
};

// stopAutoTrain() halts the auto training process and tells the user why.
trainer.prototype.stopAutoTrain = function( reason )
{	var me = this;
	me.atStartAdv = me.atTurns = false;
	me.msgCallback( reason );
};

/*
 familiars - class to hold familiar information: arena opponents, familiar strengths, your current familiar
 */

// records familiar data from KoL area page
function familiars( trainerObj )
{	var me = this;
	me.lo = trainerObj;

	// load current familiar data
	me.getBuddyInfo();
	
	// when the charpane reloads, parse it and continue other processes
	var obj = me.lo.topWnd.document.getElementsByName( 'charpane' )[0];
	obj = ( obj.wrappedJSObject || obj );
	obj.addEventListener(
		'load',
		function()
		{
			me.getBuddyInfo();
			if ( me.lo.atStartAdv && me.lo.atTurns )
				return me.lo.autoTrain();
			me.lo.updateGUI();
			me.lo.pickMatch();
		}, false );

	// load cached familiar strengths
	me.strengths = Function(	'return ' + me.lo.GM_getValue( 'famStrengths', me.emptyList() ) + ';' )();

	// store critical bits about opponents: whichopp number, weight, image filename
	me.opponents = [];
	var objs = document.getElementsByName( 'whichopp' );
	for ( var i = 0; i < objs.length; ++i )
		me.opponents[i] = { weight: objs[i].parentNode.parentNode.childNodes[2].childNodes[3].nodeValue.replace( /\s.*/g, '' ),
			whichopp: objs[i].value, img: objs[i].parentNode.nextSibling.childNodes[0].src.replace( /.+?\//g, '' )  };
};

// getBuddyInfo() gathers info on the user's current familiar
familiars.prototype.getBuddyInfo = function()
{	var me = this;
	// grab the HTML from the char pane
	var strHTML = me.lo.topWnd.document.getElementsByName( 'charpane' )[0].contentDocument.body.innerHTML;
	// the following code to get familiar weight is Tard-inspired (almost copied)
	me.buddyWeight = strHTML.indexOf( "<br>Level" ) > -1 ? strHTML.match( /\d+-pound/ )[0].replace( /-pound/, '' ) :
		strHTML.match( /\d+\slb/ )[0].replace( /\slb/, '' );
	// get the gif image associated with user's familiar
	me.buddyImg = me.lo.parse( strHTML, 'familiar.php"><img src="http://images.kingdomofloathing.com/itemimages/', '"' );
};

// emptyList() shoots back an empty list of familiar strengths
familiars.prototype.emptyList = function( callBack )
{	return "{ 'familiar4.gif': { type: 'Angry Goat', img: 'familiar4.gif', stars: [ 3, 0, 2, 1 ] } }";
};

// parseStars() translated gif image url into a number: ...amiliar%20Strengths_files/2star.gif -> 2
familiars.prototype.parseStars = function( src )
{	return Number( src.replace( /.+?\//g, '' ).replace( /\.gif/g, '' ).replace( /star/g, '' ).replace( /x/g, '0' ) );
};

// httpUpdateStrengths() contacts The Rye's website to collect updated Familiar Strengths info
familiars.prototype.httpUpdateStrengths = function( callBack )
{	var me = this;
	
	// set the loading flag to make calls depending on the data wait until it's done
	me.lo.loadingData = true;
	
	// load familiar data from The Rye's website

	me.lo.GM_xmlhttpRequest(
		{
	    method: 'GET',
	    url: 'http://www.therye.org/familiars/',
	    headers: { 'User-agent': 'Mozilla/4.0 (compatible) Greasemonkey', 'Accept': 'text/html', },
	    onload: function( responseDetails )
	    {
	    	// bail out if the webserver pukes
	    	if ( responseDetails.status != 200 )
	    	{
	    		//bail out and allow the user to use the cached data
		    	me.lo.loadingData = false;
		    	me.lo.processPending();
	    		return me.lo.msgCallback( 'Error contacing The Rye\'s website' );
	    	}
	    		
				// keep the user updated
				me.lo.msgCallback( 'Parsing familiar strengths data...' );

	    	// parse the html into DOM format
				var html = me.lo.doc.createElement( 'html' );
				html.innerHTML = responseDetails.responseText;

				// poll all input feilds to locate each record
				var objs = html.getElementsByTagName( 'input' );
				if ( !objs.length )
				{
	    		// parsing error (mysql error on site?) bail out and allow the user to use the cached data
		    	me.lo.loadingData = false;
		    	me.lo.processPending();
					return me.lo.msgCallback( 'Error contacing The Rye\'s website' );
				}
				me.strengths = new Array();
				for ( var i = 0; i < objs.length; ++i )
				{
					// if this input field isn't in a table, skip it
					if ( !objs[i].parentNode.tagName || objs[i].parentNode.tagName != 'TD' )
						continue;
				
					// grab all the <td> tags sitting around this input tag 
					var tds = objs[i].parentNode.parentNode.getElementsByTagName( 'td' );
					// parse out the name of the gif so we can match this info to that which was collected in-game
					var img = tds[1].childNodes[0].src.replace( /.+?\//g, '' );
					// lame sombrerro filename change... =/
					if ( img == 'familiar18.gif' )
						img = 'hat2.gif';
					// store the data we need: familiar type (type), the gif filename (img), familiar strengths (stars)
					me.strengths[img] = {
						type: tds[0].innerHTML, 'img': img,
						stars: [
							me.parseStars( tds[4].childNodes[0].src ), me.parseStars( tds[5].childNodes[0].src ),
							me.parseStars( tds[6].childNodes[0].src ), me.parseStars( tds[7].childNodes[0].src )
							],
						};
				}

				// build a string which represents all the collected data
				var str = '{ ';
				for ( var i in me.strengths )
				{
					str += '"' + i + '": { ';
					for ( var j in me.strengths[i] )
						if ( j != 'stars' )
							str += '"' + j + '": "' + me.strengths[i][j] + '", ';
					str += '"stars": [ ';
					for ( var j = 0; j < me.strengths[i].stars.length; ++j )
						str += me.strengths[i].stars[j] + ', ';
					str += '] }, ';
				}
				str += '}';
				
				// store that string inside GM perferences for later use (cached for a day)
				me.lo.GM_setValue( 'famStrengths', str );

				// keep the user updated
				me.lo.msgCallback( 'Caching familiar strengths data... Done.' );
				
				// increment last update field so we do this once a day
				me.lo.GM_setValue( 'lastUpdate', (new Date()).getDate() );
			
	    	// turn the loading flag off even in this error state
	    	me.lo.loadingData = false;
	    	
	    	// pump and pending calls that were waiting on this data
	    	me.lo.processPending();

				// fire off the caller's callback function (if one was given)
				if ( callBack )
					callBack();
	    }
		} );

	// tell the user the download has been queued up
	me.lo.msgCallback( 'Downloading familiar strengths from http://www.therye.org/familiars/ ...' );
};

// mybuddy() returns an object which represents the user's current familiar
familiars.prototype.mybuddy = function()
{	var me = this;
	return { name: me.strengths[me.buddyImg].type, weight: Number( me.buddyWeight ), stars: me.strengths[me.buddyImg].stars,
		img: me.buddyImg };
};

// getOpponents() returns a list of objects which represent data known about the current arena opponents
familiars.prototype.getOpponents = function()
{	var me = this;
	// return the list of opponents if we have already post-processed them
	if ( me.opponents[0].name )
		return me.opponents;
	// post-process list of opponents to include the name and strengths
	for ( var i = 0; i < me.opponents.length; ++i )
	{
		var opp = me.opponents[i];
		if ( !me.strengths[opp.img] )
		{
			// unknown strengths, ignore it
			me.opponents.splice( i, 1 );
			--i;
			continue;
		}
		opp.weight = Number( opp.weight );
		opp.name = me.strengths[opp.img].type;
		opp.stars = me.strengths[opp.img].stars;
	}
	return me.opponents;
};

// getOpponent() returns a single opponent who's whichopp value matches the one given
familiars.prototype.getOpponent = function( whichopp )
{	var me = this;
	// post process list of opponenets ( in case it hasn't already been done )
	me.getOpponents();
	// search through the list looking for a match
	for ( var i = 0; i < me.opponents.length; ++i )
		if ( me.opponents[i].whichopp == whichopp )
			return me.opponents[i];	// match found
	// no match found, panic
	return false;
};

/*
 area - class to deal purely with arena mechanics
 */

function arena( trainerObj )
{	var me = this;
	me.lo = trainerObj;
};

// runFight() simulates a single match between the given opponents in the given event
arena.prototype.runFight = function( mybuddy, enemy, event )
{	var me = this;
	var losses = 0; var fives = 0; var fours = 0; var threes = 0; var twos = 0;
	
	// calculate base ability values for each familiar
	var enemyAbility = enemy.stars[event] == 0 ? 5 : enemy.stars[event] * 3 + enemy.weight;
	var myAbility = mybuddy.stars[event] == 0 ? 5 : mybuddy.stars[event] * 3 + mybuddy.weight;
	// RNG best / worst case run through (both sides)
	for ( var n = 0; n < 5; n++ )
	{
		var myRNG = myAbility - n - 2;
		for ( var m = 0; m < 5; m++ )
		{
			var enemyRNG = enemyAbility - m - 2;
			var margin = myRNG - enemyRNG;
			if ( margin < 0 )
				losses++;
			else if ( margin <= 5 )
				fives++;
			else if ( margin == 6 )
				fours++;
			else if ( margin == 7 )
				threes++;
			else
				twos++;
		}
	}
	return { 'lose': Math.floor( (losses / 25) * 100 ), 'five': Math.floor( (fives / 25) * 100 ), 
		'four': Math.floor( (fours / 25) * 100 ), 'three': Math.floor( (threes / 25) * 100 ), 
		'two': Math.floor( (twos / 25) * 100 ) };
};

// runFights() simulates all matches between your familiar and the opponent familiars and picks 'best' match
arena.prototype.runFights = function( mybuddy, opponents, risk, fivexp, callback )
{	var me = this;
	var bestChance = 0;
	var matches = [];
	for ( var i = 0; i < opponents.length; i++ )
	{
		var enemy = opponents[i];
		for ( var j = 0; j < 4; j++ )
		{
			var odds = me.runFight( mybuddy, enemy, j );
			if ( callback )
				callback( mybuddy, enemy, odds, j );
			if ( odds.five >= bestChance && odds.lose <= risk && odds.five >= fivexp )
			{
				// this match matches the criteria and it yeilds the best chance of 5xp thus far
				bestChance = odds.five;
				var match = Object();
				match.enemy = enemy;
				match.bestEvent = j;
				match.odds = odds;
				// place it in the front of the array to indicate it's the 'best' match
				matches.splice( 0, 0, match );
			}
		}
	}
	return matches;
};
	
// start the trainer
(document.wrappedJSObject || document).trainer = new trainer();