Userscript with no name

Overhauls the new raffle page and enhances a few others

Från och med 2016-06-03. Se den senaste versionen.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// Userscript with no name - A TF2r enhancement userscript
// Copyright (C) 2016 James Lyne <[email protected]>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

// ==UserScript==
// @name        Userscript with no name
// @namespace   NiGHTS
// @author      James Lyne <[email protected]> [U:1:34673527]
// @description Overhauls the new raffle page and enhances a few others
// @include     http://tf2r.com/*
// @version     1.3.1.3
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @resource    css https://gist.githubusercontent.com/JLyne/c02c409932a14c1734c5/raw/a54b329d67cbda0396a7ee1f76837913ac8a0c83/noname-style.css
// @require     http://code.jquery.com/jquery-1.12.0.min.js
// @require     https://greasyfork.org/scripts/18834-userscript-with-no-name-skin-dictionary/code/Userscript%20with%20no%20name%20-%20Skin%20dictionary.js?version=129175
// @require		https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.0.4/jscolor.js
// @run-at      document-start
// @license     GPLv3 - http://www.gnu.org/licenses/gpl-3.0.txt
// @copyright   Copyright (C) 2016, by James Lyne <[email protected]>
// @supportURL  https://greasyfork.org/en/scripts/18644-userscript-with-no-name/feedback
// @connect     steamcommunity.com
// @noframes
// ==/UserScript==

//Tampermonkey on firefox doesn't have some of these defined for some reason
var console = window.console || {};
console.log = console.log || function() {};
console.info = console.info || console.log;
console.warn = console.warn || console.log;
console.error = console.error || console.log;
console.debug = console.debug || console.log;
console.time = console.time || console.log;
console.timeEnd = console.timeEnd || console.log;
console.trace = console.trace || console.log;
console.group = console.group || console.log;
console.groupEnd = console.groupEnd || console.log;

window.NoName = {
	init: function() {
		console.info('---Userscript with no name for TF2r v' + GM_info.script.version + '. Made with <3 by Jim :NiGHTS:---');

		this.Steam.init();
		this.UI.init();
		this.ScrapTF.init();

		if(!window.location.pathname.indexOf('/news.html')) {
			document.body.id = 'home';
		}

		if(!window.location.pathname.indexOf('/donate.html')) {
			document.body.id = 'donate';
		}

		if(!window.location.pathname.indexOf('/info.html')) {
			document.body.id = 'info';
		}

		if(!window.location.pathname.indexOf('/chat.html')) {
			document.body.id = 'chat-page';
		}

		if(!window.location.pathname.indexOf('/newraf.html')) {
			this.NewRaffle.init();
		}

		if(!window.location.pathname.indexOf('/settings.html')) {
			this.Settings.init();
		}

		if(!window.location.pathname.indexOf('/user/')) {
			this.Profile.init();
		}

		if(!window.location.pathname.indexOf('/raffles.html')) {
			this.RaffleList.init();
		} else if($('.participants').length) {
			this.Raffle.init();
		}

		if(!window.location.pathname.indexOf('/ilinks')) {
			document.body.id = 'raffle-invites';
		}
	},

	//Export override functions to unsafe window
	//This needs to be run both as early as possible and after page load
	//We can't know when our script runs relative to the scripts on the page so we need to cover both eventualities
	exportOverrides: function() {
		console.log('[NoName::exportOverrides] Exporting overrides');

		try {
			this.UI.exportOverrides();
			this.Raffle.exportOverrides();
			this.RaffleList.exportOverrides();
		} catch (e) {
			console.error(e);
		}
	},

	//Generic ajax function
	//TODO: Replace alert()s with something less shit
	AJAX: function(data) {
		var deferred = jQuery.Deferred();

		$.ajax({
			url: 'http://tf2r.com/job.php',
			type: 'POST',
			dataType: 'JSON',
			data: data,
		}).done(function(data, textStatus, jqXHR) {
			if(data.status !== 'ok') {
				alert(data.message);
			} else {
				deferred.resolve(data, textStatus, jqXHR);
			}
		}).fail(function(jqXHR, textStatus, errorThrown) {
			deferred.reject(jqXHR, textStatus, errorThrown);
		});

		return deferred.promise();
	},
};

//Generic ui changes
window.NoName.UI = {
	missingImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAMFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABaPxwLAAAAEHRSTlMAzF8Ovno2lrEpG6NtUohEM5nYxgAAAURJREFUSMdjGIHALElJSS0Bp/SkEkEwaL2MVZrrhCAchGGTDxREApUY8sxgeQRQQFfwRRAVSBugyrMKooPNhBRIoznREUPFAVQVihgKpFAVcGAoEEVVwCaIAdD88RAi2q6kDnPOB1QFC0Fi4reALCZHrGHFBLJ2AZg5Eas3mBsFxRegBEoAWmB/FLkAczD26GDfyYBfAZcBmoIJOFMWL1heAlUQM1SFcCuApB1nnPKsGG5ECxBIoIvjNGA2xIBmXPKckJgQwenJKRADPHG6oBHiAgMCXjiA04mJYHk3FDHMhCW9AKc8M4FAZOAHK0jArYAHJC/DgBswJxWCbMALkkoOMBAABgwDC2yfv8Urz9kIKnzwABPMwgdLQXMBjwJwcnmARwE4rgrwKHCE5Hvc4CCB2GawhhVv+LKVAt6Q4lK6zDAcAQAdIEKHGzsRJwAAAABJRU5ErkJggg==',

	init: function() {
		var that = this;

		console.time("NoName:UI");

		this.initUI();

		//Don't update items on new raffle page as they are replaced with the backpack below
		if(window.location.pathname.indexOf('/newraf.html') === -1) {
			this.updateItems();
		}

		NoName.Storage.listen('general:transitions', function(oldValue, newValue, url) {
			if(newValue) {
				$(document.body).addClass('transitions');
			} else {
				$(document.body).removeClass('transitions');
			}
		});

		//Apply UI colour changes
		NoName.Storage.listen('general:colour general:customcolour general:linksusecolour', function(oldValue, newValue, url) {
			NoName.UI.updateAccentColor();
		});

		console.timeEnd("NoName:UI");
	},

	exportOverrides: function() {
		var that = this;

		//Override raffle list getItems() function to handle items that don't display correctly
		unsafeWindow.getItem = exportFunction(function(item) {
			return that.getItem(item);
		}, unsafeWindow);

		//Remove slDown, message transitions are done in css now
		unsafeWindow.slDown = exportFunction(function() {}, unsafeWindow);
	},

	initUI: function() {
		console.time('NoName:UI:initUI');

		//Unbind hover event handler added by the existing js
		unsafeWindow.$('.item').unbind('mouseenter mouseleave');

		if(NoName.Storage.get('general:transitions', true)) {
			$(document.body).addClass('transitions');
		}

		//Might throw sandbox related exceptions although this should be fixed
		try {
			//ScrapTF mode cooldown
			//Export handler function to prevent sandbox issues
			unsafeWindow.$('#sendfeed').bind('click', exportFunction(function(e) {
				if(!NoName.ScrapTF.canComment()) {
					e.stopImmediatePropagation();

					return false;
				}
			 }, unsafeWindow));
		} catch (e) {
			console.warn('[Raffle:addNewUI] Unable to add unsafeWindow event handler');
		}

		$('.infitem').replaceWith($('<div></div>').addClass('infitem').append([
			$('<strong></strong>').addClass('infname'),
			$('<ul></ul>').addClass('infdesc'),
		]));

		$('table table.raffle_infomation').each(function() {
			$(this).removeClass('raffle_infomation');
			$(this).parent().addClass('raffle_infomation');
		});

		console.timeEnd('NoName:UI:initUI');
	},

	//Updates colour of the active states of various elements on the page
	//If no colors are given, itll use the currently selected custom colour or the default ones
	updateAccentColor: function(background, foreground) {
		//Remove existing styles
		$('#noname-colour').remove();

		//Fallback to saved values if needed
		if(!background || !foreground) {
			if(NoName.Storage.get('general:customcolour', false)) { //Custom colour
				var colors = NoName.Storage.get('general:colour', '#CF6A32,#222222').split(',');

				background = background || colors[0];
				foreground = foreground || colors[1];
			} else { //Default colour
				background = background || '#CF6A32';
				foreground = foreground || '#222222';
			}
		}

		var $css = $('<style></style>').prop('id', 'noname-colour'),
		css =
			'input:focus,\
			textarea:focus,\
			select:focus,\
			button:focus {\
				border: 1px solid ' + background + ';\
			}\
			input[type=submit]:hover,\
			input[type=submit]:focus,\
			input[type=submit]:active,\
			input[type=button]:hover,\
			input[type=button]:focus,\
			input[type=button]:active,\
			button:hover,\
			button:focus,\
			button:active {\
				background-color: ' + background + ';\
				border-color: ' + background + ';\
				color: ' + foreground + ';\
			}\
			.switch-field input:checked + label {\
				background-color: ' + background + ';\
				color: ' + foreground + ';\
			}\
			#settings input[type=checkbox] + label:before,\
			#settings input[type=checkbox] + span:before {\
				border-color: ' + background + ';\
				background-color: ' + background + ';\
				color: ' + foreground + ';\
			}\
			a:hover,\
			a:focus,\
			a:active,\
			.nav_font a:hover,\
			.nav_font a:focus,\
			.nav_font a:active {\
		';

		//Handle links text-shadow/colour setting
		if(NoName.Storage.get('general:linksusecolour', false)) {
			css += 'color: ' + background + ' !important;\
			text-shadow: 2px 2px 1px #000000 !important;\
			transition-property: color !important;';
		} else {
			css += 'text-shadow: 2px 2px 1px ' + background + ' !important;';
		}

		css += '}';

		$css.text(css);
		$(document.head).append($css);

		console.info('[UI::updateAccentColor] Accent colour set to ' + background);
	},

	//TODO: These 2 functions do similar things
	//Can parts be merged?
	getItem: function(item) {
		var element = document.createElement('div'),
		classes = ['item', item.q],
		information = NoName.Item.prototype.getExtendedInformation(item.name);

		//If item has extended information available then use it
		//Show fallback image if the item doesnt have one
		if(information) {
			if(information.grade) {
				element.setAttribute('iunreliable', 'true');
				classes.push('hasgrade');
				classes.push('g' + information.grade.toLowerCase());
			}

			item.name = information.name;
			item.image = (information.image || item.image || this.missingImage);
		} else {
			item.image = item.image || this.missingImage;
		}

		element.className = classes.join(' ');
		element.style.backgroundImage  = 'url(' +  item.image + ')';
		element.setAttribute('ilevel', item.level);
		element.setAttribute('iname', item.name || 'Unknown item');
		element.setAttribute('iu1', item.iu1);

		return element.outerHTML;
	},

	//Add extra information to items that don't display correctly if available
	//Otherwise display generic unknown item details
	//Used to update items not added by getItems()
	updateItems: function() {
		var that = this;

		console.time('NoName:UI:updateItems');

		$('.item').each(function() {
			var $img = $(this).children('img'),
			level = $(this).attr('ilevel'),
			name = $(this).attr('iname'),
			image = $img.attr('src'),
			information = null;

			//If an item has no image then show as unknown item
			if(!image || image === 'null') {
				name = 'Unknown item';
				image = that.missingImage;
			} else {
				information = NoName.Item.prototype.getExtendedInformation(name);

				//If item has extended information available then show it
				if(information) {
					image = information.image || image || that.missingImage;
					name = information.name;

					if(information.grade) {
						$(this).attr('iunreliable', 'true');
						$(this).addClass('hasgrade g' + information.grade.toLowerCase());
					}
				}
			}

			$img.remove();
			$(this).attr('iname', name);

			this.style.backgroundImage = 'url(' + image + ')';
			this.style.width = '';
			this.style.height = '';
		});

		console.timeEnd('NoName:UI:updateItems');
	},

	//Creates a toggle switch that can replace radio buttons
	createSwitch: function(label, name, options) {
		var $container = $('<div></div>').addClass('switch-field'),
		children = [];

		children.push($('<label></label>').addClass('switch-title').text(label));

		options.forEach(function(option, index) {
			option.id = option.id || name.replace(' ', '-').toLowerCase() + '-' + index;
			option.value = (typeof option.value !== 'undefined') ? option.value : '';

			children.push(
				$('<input />')
				.prop({
					type: 'radio',
					name: name,
					id: option.id,
					checked: !!option.checked,
				}).val(option.value)
			);
			children.push(
				$('<label></label>')
				.prop('for', option.id)
				.text(option.label)
			);
		});

		$container.append(children);

		return $container;
	},

	//Item details tooltip
	//Used everywhere items are shown
	//Except the new raffle page which uses its own implementation due to different data
	showItemInfo: function(item) {
		var $item = $(item),
		name = $item.attr('iname'),
		level = $item.attr('ilevel'),
		series = $item.attr('iu1'),

		pos = $item.offset(),
		height = $item.height(),
		width = $item.width();

		//Need all item classes other than .item so this'll do
		$('.infname').addClass(item.className).removeClass('item').html(name);
		$('.infdesc').html('<li>Level: ' + level + ((series) ? '<br />#' + series : '') + '</li>');

		if($item.attr('iunreliable')) {
			//TODO: style this properly
			$('.infdesc').append('<li style="color:red;">Factory new variant shown, actual item will differ</li>');
		}

		$('.infitem').show().css({
			left: pos.left + (width / 2) - ($('.infdesc').outerWidth() / 2) + 'px',
			top: (pos.top + height + 4) + 'px'
		});
	},

	hideItemInfo: function() {
		$('.infitem').hide();
		$('.infname').removeClass().addClass('infname');
	},

	addStyles: function() {
		GM_addStyle(GM_getResourceText('css'));
		NoName.UI.updateAccentColor();
	},
};

//Profile pages
window.NoName.Profile = {
	init: function() {
		document.body.id = 'profile';
		this.initUI();
	},

	initUI: function() {
		var progress,
		$name;

		//Optimisation to avoid excessive style calculations in firefox
		//Detach containing element before making UI changes
		this.$UI = $('#content').detach();
		progress = this.calculateProgression(this.$UI.find('.upvb'));

		//Add ids to things for css styling
		$name = this.$UI.find('td:nth-child(2) > div > a').prop('id', 'name');
		this.$UI.find('.indent > table tr:nth-child(3) > td:nth-child(2)').prop('id', 'rep');
		this.$UI.find('.indent > table > tbody > tr:nth-child(2) > td').prop('id', 'rank');
		this.$UI.find('.indent > table tr:nth-child(2) > td > table tr:nth-child(2) > td:nth-child(2)').prop('id', 'progress');

		//Fix progress bar value and update colour to match username colour
		this.$UI.find('#progress div > div').first().css({
			'background-color': $name.css('color'),
			'font-size': 0,
			width: Math.min(progress, 100) + '%',
			'border-radius': (progress >= 100) ? '3px' : '',
		}).next().text(progress.toFixed() + '%');

		this.initFeedbackForm();
		this.checkMoreUserInfo();

		//Reattach updated UI
		this.$UI.insertAfter('#nav_holder');
	},

	initFeedbackForm: function() {
		var $button = this.$UI.find('.nfbutton'),
		$form = this.$UI.find('.newfeed').empty(),
		$typeSwitch = NoName.UI.createSwitch('Type:', 'type', [
			{
				id: 'type1',
				label: 'Positive',
				value: '1',
			},
			{
				id: 'type2',
				label: 'Negative',
				value: '2',
			},
			{
				id: 'type0',
				label: 'Neutral',
				value: '0',
				checked: true,
			},
		]),

		$elements = [
			$typeSwitch, //Type Switch
			$('<label></label>').text('Message:'), //Message Label
			$('<textarea></textarea>').prop('id', 'feedtext').addClass('full-width'), //Message textarea
			$('<button></button>').prop({ //Submit button
				type: 'button',
				id: 'sendfeed',
			}).text('Post'),
		];

		$form.append($elements);
	},

	//Update rep table width if the more user info script is detected
	//to make it look nicer
	checkMoreUserInfo: function() {
		var $rep = this.$UI.find('#rep'),
		$button = this.$UI.find('.indent .nfbutton input'),
		observer;

		if($button.val() === 'Post new comment') {

		}

		if($rep.children().length > 1) {
			$rep.css('width', '100%');

			return;
		}

		observer = new MutationObserver(function(mutations) {
			$rep.css('width', '100%');
		});

		observer.observe($rep.get(0), {
			childList: true,
		});
	},

	addLoadingPlaceholders: function() {
		
	},

	calculateProgression: function($rep) {
		var rep = parseInt($rep.text()),
		progress = 0;

		if(rep < 1000) {
		    progress = rep / 1000;
		} else if(rep < 2500) {
	    	progress = (rep - 1000) / 1500;
		} else if(rep < 5000) {
	    	progress = (rep - 2500) / 2500;
		} else {
		   	progress = rep / 5000;
		}

		progress *= 100;

		return progress;
	},
};

//Settings page
window.NoName.Settings = {
	$settings: null,
	sections: {},

	init: function() {
		document.body.id = 'settings';

		this.initUI();
		this.addSettingsContainer();
		this.addScriptSettings();
	},

	initUI: function() {
		//Remove old radio buttons for raffle icon position
		$('#fselec').next().nextAll().remove();
		$('#fselec').prev().remove();

		var $iconWarning = $('#fselec').closest('.raffle_infomation').find('h3'),
		$position = NoName.UI.createSwitch('Icon position:', 'position', [
			{
				id: 'tl',
				label: 'Top-left',
				value: 'tl',
				checked: true,
			},
			{
				id: 'tr',
				label: 'Top-right',
				value: 'tr',
			},
			{
				id: 'bl',
				label: 'Bottom-left',
				value: 'bl',
			},
			{
				id: 'br',
				label: 'Bottom-right',
				value: 'br',
			}
		]),
		$fileName = $('<span></span>').prop('id', 'fname').text('Click to choose file');

		//Add filename placeholder and position switch
		$('#fselec').after($position.hide()).after($fileName);

		//Update placeholder text when hidden input changes
		$('#fselec').on('change', function() {
			var file = this.files[0];

			if(file) {
				$position.show();
				$fileName.text(file.name);
			} else {
				$position.hide();
				$fileName.text('Click to choose file');
			}
		});

		//Reformat sidepic warning in a nicer looking way
		$iconWarning.replaceWith(
			$('<strong></strong>').html($iconWarning.text().replace('	Use', '<br />Use'))
		);
	},

	//Add a new section for our own settings
	addSettingsContainer: function() {
		this.$settings = $('<div></div>').addClass('raffle_infomation').prop('id', 'noname-settings')
			.append([
				$('<h1></h1>').text('Userscript with no name')
			]);

		$('#content > .indent').append('<br />').append(this.$settings);
	},

	addSettingsSection: function(id, title) {
		if(this.sections[id]) {
			console.error('[Settings::addSettingsSection] Duplicate id ' + id);

			return false;
		}

		var $section = $('<div></div>').append(
			$('<h2></h2>').text(title)
		).prop('id', id).addClass('settings-section');

		this.$settings.append($section);
		this.sections[id] = $section;

		return true;
	},

	addToSection: function(id, $elements) {
		if(!this.sections[id]) {
			console.error('[Settings::addToSection] Invalid id ' + id);

			return false;
		}

		this.sections[id].append($elements);

		return true;
	},

	//Add script settings to new section
	addScriptSettings: function() {
		this.addGeneralSettings();
		this.addRaffleSettings();
		this.addOtherSettings();
		this.addAbout();
	},

	addGeneralSettings: function() {
		var storage = NoName.Storage,
		transitionsEnabled = storage.get('general:transitions', true),
		linksUseColour = storage.get('general:linksusecolour', false),
		customColor = storage.get('general:customcolour', false),
		color = storage.get('general:colour', '#CF6A32,#222222').split(','),

		//UI Color
		$color = NoName.UI.createSwitch('Accent Colour: ', 'customcolor', [
			{
				label: 'Default',
				checked: !customColor,
			},
			{
				label: 'Custom',
				value: true,
				checked: customColor,
			}
		]),

		$colorpicker = $('<input />').addClass('jscolor').attr({
			id: 'accent-colorpicker',
			'data-jscolor': '{hash:true, padding:0, shadow:false, borderWidth:0, backgroundColor:\'transparent\', insetColor:\'#000\', width: 256}',
		}).val(color[0]),

		//Link highlighting
		$links = NoName.UI.createSwitch('Link hover effect: ', 'links', [
			{
				label: 'Text Shadow',
				checked: !linksUseColour,
			},
			{
				label: 'Text Colour',
				value: true,
				checked: linksUseColour,
			}
		]),

		//Transitions
		$transitions = NoName.UI.createSwitch('Animations: ', 'transitions', [
			{
				label: 'Disabled',
				checked: !transitionsEnabled,
			},
			{
				label: 'Enabled',
				value: true,
				checked: transitionsEnabled,
			}
		]);

		$color.append($colorpicker);

		if(!customColor) {
			$colorpicker.hide();
		}

		$color.on('change', function(e) {
			if(e.target.value) {
				$colorpicker.show();
			} else {
				$colorpicker.hide();
			}

			storage.set('general:customcolour', e.target.value);
		});

		$colorpicker.on('change', function(e) {
			var background = this.jscolor.toHEXString(),
			foreground = '#FFFFFF',
			rgb = parseInt(background.substring(1), 16),
			r = (rgb >> 16) & 0xff,
			g = (rgb >> 8) & 0xff,
			b = (rgb >> 0) & 0xff,
			luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;

			//Use darker text if the background colour is light
			if(luma > 128) {
			    foreground = '#222222';
			}
			var colors = [
				background,
				foreground,
			].join(',');

			storage.set('general:colour', colors);
		});

		$links.on('change', function(e) {
			storage.set('general:linksusecolour', e.target.value);
		});

		$transitions.on('change', function(e) {
			storage.set('general:transitions', e.target.value);
		});

		this.addSettingsSection('general', 'General');

		this.addToSection('general', [
			$color,
			$links,
			$transitions,
		]);
	},

	addRaffleSettings: function() {
		var storage = NoName.Storage,
		showAllItemsEnabled = storage.get('raffles:showallitems', false),
		raffleLayout = storage.get('raffles:rafflelayout', false),

		//Show all items in raffle list
		$showAllItems = NoName.UI.createSwitch('Raffle list - Show all items: ', 'show-all-items', [
			{
				label: 'Disabled',
				checked: !showAllItemsEnabled,
			},
			{
				label: 'Enabled',
				value: true,
				checked: showAllItemsEnabled,
			}
		]),

		//Alternate raffle layout
		$raffleLayout = NoName.UI.createSwitch('Raffle - Alternate Layout:', 'raffle-layout', [
			{
				label: 'Disabled',
				checked: !raffleLayout,
			},
			{
				label: 'Enabled',
				value: true,
				checked: raffleLayout,
			}
		]);

		$showAllItems.on('change', function(e) {
			storage.set('raffles:showallitems', e.target.value);
		});

		$raffleLayout.on('change', function(e) {
			storage.set('raffles:rafflelayout', e.target.value);
		});

		this.addSettingsSection('raffles', 'Raffles');

		this.addToSection('raffles', [
			$raffleLayout,
			$showAllItems,
		]);
	},

	addOtherSettings: function() {
		var storage = NoName.Storage,
		scrapEnabled = storage.get('scrap:enabled', false),

		//ScrapTF mode
		$scrap = NoName.UI.createSwitch('ScrapTF Mode:', 'scraptf-mode', [
			{
				label: 'Disabled',
				checked: !scrapEnabled,
			},
			{
				label: 'Enabled',
				value: true,
				checked: scrapEnabled,
			}
		]);

		//Enable scrapTF mode or check if it can be disabled
		$scrap.on('change', function(e) {
			if(e.target.value) {
				NoName.ScrapTF.enable();
			} else if(!NoName.ScrapTF.disable()) {
				$('#scraptf-mode-1').prop('checked', true);
			}
		});


		this.addSettingsSection('other', 'Other');
		this.addToSection('other', [
			$scrap,
		]);
	},

	addAbout: function() {
		this.addSettingsSection('about', 'About');
		this.addToSection('about', [
			$('<p></p>').text('Userscript with no name v' + GM_info.script.version + '.\n©2016 James Lyne. Licensed under GPL-3.0.'),
			$('<a></a>').prop('href', 'http://www.gnu.org/licenses/gpl-3.0.txt').text('License'),
			' - ',
			$('<a></a>').prop('href', 'https://greasyfork.org/en/scripts/18644-userscript-with-no-name').text('Changelog'),
			' - ',
			$('<a></a>').prop('href', 'https://greasyfork.org/en/scripts/18644-userscript-with-no-name/feedback').text('Feedback'),
		]);
	}
};

//New raffle page
window.NoName.NewRaffle = {
	$itemList: null,
	$selectedItemList: null,
	$oldItems: null,
	$banWarning: null,

	$visibility: null,
	$entry: null,
	$type: null,
	$start: null,

	$submit: null,
	$UI: null,

	backpack: null,
	levelData: {},

	init: function() {
		var that = this;

		console.time("NoName:NewRaffle");

		document.body.id = 'new-raffle';

		this.$itemList = $('#allitems');
		this.$selectedItemList = $('#selitems');
		this.$banWarning = $('.ban_warning');

		this.$visibility = $('#ptype1').parent();
		this.$entry = $('#ptype2').parent();
		this.$type = $('#af1').parent();
		this.$start = $('#af2').parent();

		this.$submit = $('#rafBut').parent();

		this.initUI();
		this.addSwitches();

		this.backpack = new NoName.Backpack({
			container: this.$itemList,
			selectedContainer: this.$selectedItemList,
			autoRender: true,
			tradeableOnly: true,
			autoLoad: true,
			selectableItems: true,
		});

		console.timeEnd("NoName:NewRaffle");
	},

	addSwitches: function() {
		var $visibility = NoName.UI.createSwitch('Raffle visiblity:', 'rafflepub', [
			{
				id: 'ptype1',
				label: 'Public',
				value: 'public',
				checked: true,
			},
			{
				id: 'ptype2',
				label: 'Private',
				value: 'private',
			}
		]);

		var $entry = NoName.UI.createSwitch('Entry type:', 'invo', [
			{
				label: 'Open',
				id: 'af1',
				value: 'false',
				checked: true,
			},
			{
				id: 'af2',
				label: 'Invite only',
				value: 'true',
			}
		]);

		var $type = NoName.UI.createSwitch('Prize distribution:', 'split', [
			{
				id: 'isplit1',
				label: 'A21',
				value: 'alltoone',
			},
			{
				id: 'isplit2',
				label: '121',
				value: 'onerperson',
				checked: true
			}
		]);

		var $start = NoName.UI.createSwitch('Start timer:', 'stype', [
			{
				id: 'stype1',
				label: 'Instantly',
				value: 'instantly',
			},
			{
				id: 'stype2',
				label: 'After first entry',
				value: 'afterjoin',
				checked: true
			}
		]);

		//Add radio button replacement toggles
		this.$visibility.append($visibility);
		this.$entry.append($entry);
		this.$type.append($type);
		this.$start.append($start);
	},

	initUI: function() {
		//Optimisation to avoid excessive style calculations in firefox
		//Detach containing element before making UI changes
		this.$UI = $('#content').detach();

		this.removeExistingUI();

		//Detach entries and referer, also add :s for consistency
		var that = this,
		$entries = [
			this.$UI.find('#maxentry').parent().prev().text('Maximum entries:').detach(),
			this.$UI.find('#maxentry').parent().detach()
		],

		$referer = [
			$('<td></td>').prop('colspan', 2),
			this.$UI.find('#reffil').parent().prev().text('Referal filter:').detach(),
			this.$UI.find('#reffil').parent().detach()
		];

		//Move entries after duration
		this.$UI.find('#durr').parent().after($entries);

		//Move referer to a new tr after duration/entries
		this.$UI.find('#durr').closest('tr').after(
			$('<tr></tr>').append($referer)
		);

		//Change defaults and other attributes to more sensible values
		this.$UI.find('#rtitle').addClass('full-width').prop({
			placeholder: 'Raffle title',
			maxlength: 32,
			onclick: null,
		}).val('');

		this.$UI.find('#mess').parent().prop('colspan', 3);
		this.$UI.find('#mess').prop({
			maxlength: 2048,
		});

		this.$UI.find('#durr').addClass('full-width').val(3600);

		this.$UI.find('#maxentry').addClass('full-width').prop({
			type: 'number',
		}).val(1000);

		this.$UI.find('#reffil').addClass('full-width').prop('placeholder', '*').val('');

		//Make selected items style consistent with backpack items
		this.$selectedItemList.addClass('itemtable');

		//Add <colgroup> to form table to make column widths consistent
		this.$UI.find('.text_holder table').prepend(
			$('<colgroup></colgroup>').append([
				$('<col />').css('width', '19%'), //Account for padding on 3rd column. Not nice but calc() doesn't work properly here.
				$('<col />').css('width', '30%'),
				$('<col />').css('width', '20%'),
				$('<col />').css('width', '30%'),
			])
		);

		//TODO: perhaps just overwrite the existing create function via unsafeWindow?
		this.$submit.append(
			$('<button></button>').prop({
				type: 'button',
				id: 'raffle-button' //Different id to prevent old event handler triggering
			}).addClass('full-width')
			.text('Raffle it!')
			.on('click', function() {
				that.createRaffle();
			})
		);

		//Lock visibility to private when invite only is selected
		this.$UI.on('change', '#af1, #af2', function() {
			if(this.value === 'true') {
				$('#ptype2').prop({
					checked: true,
				});
			}

			$('#ptype1, #ptype2').prop('disabled', this.value === 'true');
		});

		//Reattach updated UI
		this.$UI.insertAfter('#nav_holder');
	},

	removeExistingUI: function() {
		//Remove games selection
		this.$UI.find('#allgames').parent().prev().remove();
		this.$UI.find('#allgames').parent().remove();

		//Remove remaining unneeded radio button <tr>s
		this.$UI.find('#isplit1').closest('tr').remove();
		this.$UI.find('#stype1').closest('tr').remove();

		//I'm sure anyone using this already knows the rules
		this.$banWarning.remove();

		//Empty things we are going to replace
		this.$visibility.empty();
		this.$entry.empty();
		this.$type.empty();
		this.$start.empty();

		//Remove existing button so I can readd it again without existing event handlers
		this.$UI.find('#rafBut').remove();
		this.$UI.find('.infitem').remove();
		this.$UI.find('#rtitle').removeAttr('size');
	},

	createRaffle: function() {
		var items = [];

		$('#raffle-button').prop('disabled', true).val('Please wait...');

		this.backpack.getSelected().forEach(function(item) {
			var data = [
				item.getDefIndex(),
				item.getQuality(),
				item.getLevel(),
				''//item.getSeries() //This is always empty in the original inventory apparently
			];

			items.push(data.join(':'));
		});

		NoName.AJAX({
			postraffle: 'true',
			title: $('#rtitle').val(),
			message: $('#mess').val(),
			maxentry: $('#maxentry').val(),
			duration: $('#durr').val(),
			filter: $('#reffil').val() || '*',
			split: $('input[name=split]:checked').val(),
			pub: $('input[name=rafflepub]:checked').val(),
			stype: $('input[name=stype]:checked').val(),
			invo: $('input[name=invo]:checked').val(),
			items: items,
			games: [],
		}).done(function(data) {
			//TODO: track raffled items here

			window.location.href = 'http://tf2r.com/k' + data.key;
		}).always(function() {
			$('#raffle-button').prop('disabled', false).val('Raffle it!');
		});
	},
};

window.NoName.RaffleList = {
	init: function() {
		var that = this;

		document.body.id = 'raffle-list';

		this.initUI();

		//Update item lists if show all setting changes
		NoName.Storage.listen('raffles:showallitems', function(oldValue, newValue, url) {
			that.getItems();
		});

		//Hack to fix timing issues for now
		this.getItems();
	},

	exportOverrides: function() {
		var that = this;

		//Override getItems function to add +x overflow and optional showing of all items
		unsafeWindow.getItems = exportFunction(function() {
			return that.getItems();
		}, unsafeWindow);

		//Override check raffles function to remove display: none from raffle header
		unsafeWindow.checkraffles = exportFunction(function() {
			return that.checkraffles();
		}, unsafeWindow);

		//Remove ih() function as we're replacing it with our own implementation
		unsafeWindow.ih = exportFunction(function() {}, unsafeWindow);
	},

	initUI: function() {
		$('.participants').on('mouseover', '.item', function() {
			NoName.UI.showItemInfo(this);
		}).on('mouseout', '.item', NoName.UI.hideItemInfo);

		if(NoName.ScrapTF.isEnabled()) {
			$('.pubrhead-text-right a').each(function() {
				if(NoName.ScrapTF.removedByStaff()) {
					console.warn('[ScrapTF::removedByStaff] Raffle title removed by staff');
					$(this).html('<code>[Removed by staff]</code>');
				}
			});
		}
	},

	//TODO: This repeats a lot of what getitems() does
	//Can they be merged?
	checkraffles: function() {
		var that = this;

		if(!unsafeWindow.focused) {
			setTimeout(unsafeWindow.checkraffles, 5000);

			return;
		}

		NoName.AJAX({
			'checkpublicraffles': 'true',
			'lastpraffle': unsafeWindow.lpr,
		}).done(function(data) {
			if(data.message.newraf.length) {
				that.populateRaffles(data.message.newraf);
			}

			unsafeWindow.ih();
		});

		setTimeout(unsafeWindow.checkraffles, 5000);
	},

	getItems: function() {
		var list = [],
		that = this;

		$('.jqueryitemsgather').each(function(index, object) {
			list[index] = $(object).attr('rqitems');
		});

		NoName.AJAX({
			'getitems': 'true',
			'list': list.join(';'),
		}).done(function(data){
			if(data.message.items) {
				that.populateRaffleItems(data.message.items);
			}
		});
	},

	populateRaffles: function(raffles) {
		var showAll = NoName.Storage.get('raffles:showallitems', false);

		for(var id in raffles) {
			var ent = raffles[id],
			itemlist = '',
			remaining = 0,
			wid = 644,
			$header,
			$content;

			for(var iid in ent.items) {
				var eent = ent.items[iid];

				//Leave space for "+x" if show all items is disabled
				if(!showAll && (wid -= eent.wid) <= 74) {
					remaining++;
					continue;
				}

				itemlist += unsafeWindow.getItem(eent);
			}

			if(unsafeWindow.lpr < ent.id) {
				unsafeWindow.lpr = ent.id;
			}

			if(NoName.ScrapTF.isEnabled() && NoName.ScrapTF.removedByStaff()) {
				console.warn('[ScrapTF::removedByStaff] Raffle title removed by staff');
				ent.name = '<code>[Removed by Staff]</code>';
			}

			//Not proud of this but theres a lot of html to build
			//Raffle header
			$header = $('<div></div>').addClass('pubrhead').append(
				$('<div></div>').addClass('pubrhead-text-left').append(
					$('<a></a>') //Username
					.prop('href', ent.link)
					.css('color', '#' + ent.color)
					.html(ent.name) //Already escaped
				).append(
					$('<div></div>').addClass('pubrhead-text-right').append(
						$('<a></a>').prop('href', ent.rlink).text(ent.rname) //Raffle name
					)
				).append(
					$('<div></div>').addClass('pubrhead-arrow-border')
				).append(
					$('<div></div>').addClass('pubrhead-arrow')
				)
			);

			//Raffle content
			$content = $('<div></div>').addClass('pubrcont').append(
				$('<div></div>').addClass('pubrleft').append(
					$('<div></div>').addClass('pubrav').append(
						$('<a></a>').prop('href', ent.link).append(
							$('<img />').prop('src', ent.avatar).css({ //User avatar
								width: '64px',
								height: '64px',
							})
						)
					)
				).append(
					$('<div></div>').addClass('pubrarro')
				)
			).append(
				$('<div></div>').addClass('pubrright').html(itemlist) //Items
				.attr('data-overflow', (remaining && !showAll) ? ('+' + remaining) : undefined)
			);

			$('.participants').prepend('<div class="clear"></div>').prepend($content).prepend($header);
		}
	},

	populateRaffleItems: function(items) {
		var showAll = NoName.Storage.get('raffles:showallitems', false);

		$('.jqueryitemsgather').empty().each(function() {
			var width = $(this).width() - 74,
			raffle = $(this).attr('rqitems'),
			remaining = 0;

			$(this).removeAttr('data-overflow');

			for(var id in items) {
				var ent = items[id];

				if(ent.rkey != raffle) {
					continue;
				}

				//Leave space for "+x" if show all items is disabled
				if(!showAll && (width -= ent.wid) <= 74) {
					remaining++;
					continue;
				}

				$(this).append(unsafeWindow.getItem(ent));
			}

			//Add +x if there are any undisplayed items
			if(remaining && !showAll) {
				$(this).attr('data-overflow', '+' + remaining);
			}
		});
	},
};

window.NoName.Raffle = {
	raffleID: '',

	$statsContainer: null,

	$message: null,
	$winChance: null,
	$timeLeft: null,
	$entries: null,

	entries: 0,
	winChance: 100,
	itemCount: 1,
	timeLeft: 0,
	type: null,

	init: function() {
		var that = this;

		console.time('NoName:Raffle');

		document.body.id = 'raffle';

		//TODO: Are raffle ids always 6 characters?
		this.raffleID = window.location.pathname.substring(2, 8);

		this.$timeLeft = $('#tlefttd');
		this.$message = $('td[colspan="3"]').first();
		this.$entries = $('#entry');
		this.$winChance = $('#winc');
		this.$type = null;

		this.entries = parseInt(this.$entries.text(), 10);
		this.itemCount = $('.raffle_infomation .item').length;
		this.timeLeft = parseInt(unsafeWindow.tleft, 10);

		this.initUI();
		this.updateTimer();
		this.checkRaffle(true);

		NoName.Storage.listen('raffles:rafflelayout', function() {
			unsafeWindow.location.reload();
		});

		console.timeEnd('NoName:Raffle');
	},

	exportOverrides: function() {
		var that = this;

		//Override checkraffle to fix some things
		unsafeWindow.checkraffle = exportFunction(function() {
			return that.checkRaffle();
		}, unsafeWindow);

		unsafeWindow.updateTimer = exportFunction(function() {}, unsafeWindow);

		//Remove existing updateWC
		unsafeWindow.updateWC = exportFunction(function() {}, unsafeWindow);
	},

	initUI: function() {
		var that = this;

		//Optimisation to avoid excessive style calculations in firefox
		//Detach containing element before making UI changes
		this.$UI = $('#content').detach();

		//Add ids to things for css styling
		this.$UI.find('.indent table:nth-child(2) > tbody > tr:nth-child(5) > td:nth-child(2)').prop('id', 'rep');
		this.$UI.find('.indent table:nth-child(2) tr:nth-child(7) td').prop('id', 'prizes');
		this.$UI.find('.indent table:nth-child(2) > tbody > tr:nth-child(1) > td > div').prop('id', 'raffle-title');

		//Add load more comments button if there are 50 comments
		if(this.$UI.find('.userfeedpost').length == 50) {
			this.$UI.find('.userfeed').append(
				$('<button></button>').prop('id', 'load-comments').text('Load more comments...').on('click', function() {
					that.loadComments();
				})
			);
		}

		//Replace raffle delivery links with buttons
		this.$UI.find('.spectik').each(function() {
			var type = $(this).attr('id'),
			wid = $(this).attr('wid'),
			text = $(this).text();

			$(this).replaceWith(
				$('<button></button>').addClass('spectik').data({
					win: wid,
					option: type,
				}).prop('type', 'button').attr('data-type', type).text(text)
			);
		});

		//Load new raffle layout if enabled
		if(NoName.Storage.get('raffles:rafflelayout', false)) {
			this.addRaffleStats();
		} else {
			this.$type = $('<td></td>').prop('id', 'type').text('...');

			this.$UI.find('#entry').parent().after(
				$('<tr></tr>').append([
					$('<td></td>').text('Type:'),
					this.$type,
				])
			);
		}

		if(NoName.ScrapTF.isEnabled() && NoName.ScrapTF.removedByStaff()) {
			this.$UI.find('#raffle-message').html('<code>[Removed by staff]</code>');
			this.$UI.find('#raffle-title').html('<code>[Removed by staff]</code>');
		}

		this.initCommentForm();
		this.initEvents();

		//Reattach updated UI
		this.$UI.insertAfter('#nav_holder');
	},

	initCommentForm: function() {
		var $button = this.$UI.find('#newfeed'),
		$form = this.$UI.find('.newfeed').empty(),

		$elements = [
			$('<label></label>').text('Message:'), //Message Label
			$('<textarea></textarea>').prop('id', 'feedtext').addClass('full-width'), //Message textarea
			$('<button></button>').prop({ //Submit button
				type: 'button',
				id: 'sendfeed',
			}).text('Post'),
		];

		$button.val('Post new comment');
		$form.append($elements);
	},

	initEvents: function() {
		var that = this;

		//New hover handler
		this.$UI.find('.indent').on('mouseover', '.item', function() {
			//Timing issues mean we can't be sure the original hover events are gone
			unsafeWindow.$('.item').unbind('mouseenter mouseleave');

			NoName.UI.showItemInfo(this);
		}).on('mouseout', '.item', function() {
			NoName.UI.hideItemInfo();
		});

		//Remove original event handler on first click
		this.$UI.find('.spectik').one('click', function(e) {
			unsafeWindow.$('.spectik').unbind('click');
		});		

		this.$UI.find('.participants_winner').on('click', '.spectik', function(e) {
			var data = $(this).data();

			data.tik = true;
			data.rid = that.raffleID;

			$(e.target).parent().addClass('loading');

			NoName.AJAX(data).done(function() {
				$(e.target).prop('disabled', true).siblings().remove();
			}).always(function() {
				$(e.target).parent().removeClass('loading');
			});

			return false;
		});
	},

	checkRaffle: function(once) {
		var that = this;

		NoName.AJAX({
			'checkraffle': 'true',
			'rid': this.raffleID,
			'lastentrys': unsafeWindow.entryc,
			'lastchat': unsafeWindow.lastchat,
		}).done(function(data) {
            var ent;

			if(data.message.ended && !unsafeWindow.ended) {
				window.location.reload();
			}

			$('#entry').html(data.message.cur_entry + '/' + data.message.max_entry);

			that.entries = data.message.cur_entry;
			unsafeWindow.entryc = data.message.entry;
			unsafeWindow.tleft = that.timeLeft = data.message.timeleft;
			unsafeWindow.nwc = that.winChance = data.message.wc;
			unsafeWindow.lastchat = data.message.chatmax;

			that.determineRaffleType();
			that.updateWC();

			if(!unsafeWindow.started && data.message.started) {
				unsafeWindow.started = true;
				that.updateTimer();
			}

			that.addComments(data.message.chaten, true);
			that.addParticipants(data.message.newentry);
		});

		if(!once) {
			setTimeout(unsafeWindow.checkraffle, (unsafeWindow.ended) ? 5000 : 3500);
		}
	},

	updateTimer: function(once) {
		var that = this;

		if(!unsafeWindow.started) {
			return;
		}

		this.timeLeft--;

		if(unsafeWindow.ended || this.timeLeft < 0) {
			this.$timeLeft.text('Ended');

			return;
		}

		var hours = Math.floor(this.timeLeft / 3600),
		minutes = Math.floor(this.timeLeft / 60 - hours * 60),
		seconds = Math.floor(this.timeLeft - hours * 3600 - minutes * 60),
		time = [];

		hours && time.push(hours + 'h'); //Removed leading 0 to fix >=100 hour raffles
		minutes && time.push(('00' + minutes).slice(-2) + 'm');
        seconds && time.push(('00' + seconds).slice(-2) + 's');

		this.$timeLeft.text(time.join(' '));

		if(!once) {
			setTimeout(function() {
				that.updateTimer();
			}, 1000);
		}
	},

	updateWC: function() {
		var that = this;

		//Break loop if remaining difference is too small to be displayed
		if((unsafeWindow.cwc - this.winChance) < 0.0005) {
			return;
		}

		unsafeWindow.cwc -= (unsafeWindow.cwc - this.winChance) / 10;
		this.$winChance.html(unsafeWindow.cwc.toFixed(3) + '%');

		setTimeout(function() {
			that.updateWC();
		}, 50);
	},

	//Determine if this is an A21 or 121 raffle using the win chance and number of entries
	determineRaffleType: function() {
		//Don't calculate again if we've already done it
		if(this.type) {
			return;
		}

		//Need at least 2 entries to determine type
		if(this.entries <= 1) {
			return;
		}

		var winners = Math.round((this.winChance * this.entries) / 100);

		if(winners > 1) {
			this.type = '121';
			this.$type.empty().append(
				$('<abbr><abbr/>').prop('title', 'One to one').text('121')
			);
		} else {
			this.type = 'A21';
			this.$type.empty().append(
				$('<abbr><abbr/>').prop('title', 'All to one').text('A21')
			);
		}
	},

	//Replace old entries/time/chance stats with some new fancy looking ones
	addRaffleStats: function() {
		var message = this.$message.html();

		//Remove nested table as it only contains one cell we need, the raffle message
		this.$message = this.$message.closest('.raffle_infomation');
		this.$message.prop('id', 'raffle-message').html(message);

		//Remove things we dont need anymore
		this.$entries.closest('tr').remove();

		//TODO: These should be moved somewhere instead of removed
		this.$UI.find('td[data-rstart-unix]').closest('tr').remove();
		this.$UI.find('td[data-rsend-unix]').closest('tr').remove();

		//Create new <tr> and <td> for raffle stats
		this.$statsContainer = $('<td></td>').addClass('raffle_infomation').prop('id', 'raffle-stats');
		this.$message.parent().after(
			$('<tr></tr>').append(this.$statsContainer)
		);

		//Account for extra row on avatar <td>
		this.$UI.find('td[rowspan="2"]').attr('rowspan', '3');

		this.$entries = $('<strong></strong>').prop('id', 'entry').text('...');
		this.$winChance = $('<strong></strong>').prop('id', 'winc').text('...');
		this.$timeLeft = $('<strong></strong>').prop('id', 'tlefttd').text('...');
		this.$type = $('<strong></strong>').prop('id', 'type').text('...');

		this.$statsContainer.append([
			this.$timeLeft,
			this.$entries,
			this.$winChance,
			this.$type,
		]);
	},

	loadComments: function() {
		var that = this;

		$('#load-comments').prop('disabled', true).text('Loading...');

		NoName.AJAX({
			'checkraffle': 'true',
			'rid': this.raffleID,
			'lastentrys': unsafeWindow.entryc,
			'lastchat': 0 //All comments
		}).done(function(data) {
			//Remove first 50 comments that are already on the page
			var comments = data.message.chaten.slice(0, -50).reverse();
			that.addComments(comments);

			$('#load-comments').remove();
		});
	},

	addComments: function(comments, prepend) {
		if(prepend) {
			comments = comments.reverse();
		}

		for(var id in comments) {
			var comment = comments[id],

			//Comment container
			$container = $('<div></div>').addClass('userfeedpost').css({
				'background-color': '#' + comment.color,
			}),

			//Username
			$username = $('<div></div>').addClass('ufinf').append(
				$('<div></div>').addClass('ufname').append(
					$('<a></a>').prop('href', comment.url).css({
						color: '#' + comment.color,
					}).text(comment.name)
				)
			),

			//Avatar
			$avatar = $('<div></div>').addClass('ufavatar').append(
				$('<a></a>').prop('href', comment.url).append(
					$('<img />').prop('src', comment.avatar)
				)
			),

			//Message
			$message = $('<div></div>').addClass('ufmes').html(comment.message); //Already sanitised

			//Append them all together
			$container.append([
				$username.append($avatar),
				$message
			]);

			if(prepend) {
				$('.userfeed').prepend($container);
			} else {
				$('.userfeed').append($container);
			}
		}
	},

	addParticipants: function(participants) {
		for(var id in participants) {
			var participant = participants[id];

			if(unsafeWindow.lastname != participant.name) {
				$('#pholder').prepend('<div class="pentry"><div class="pavatar"><a href="' + participant.link + '"><img src="' + participant.avatar + '" width="64px" height="64px" /></a></div><div class="pname"><a href="' + participant.link + '" style="color:#' + participant.color + ';">' + participant.name + '</a></div></div>');
			}

			unsafeWindow.lastname = participant.name;
		}
	},
};

//Emulates the... interesting choice to add sizeable cooldowns to everything in scrapTF
//Would be a good idea to not take this seriously, just saying, let me have my fun.
//TODO: Auctions?
window.NoName.ScrapTF = {
	enabled: 0,
	lastEntry: 0,
	lastComment: 0,

	COOLDOWN_RAFFLE: 10,
	COOLDOWN_COMMENT: 30,
	COOLDOWN_DISABLE: 30,
	REMOVED_CHANCE: 0.2,

	init: function() {
		var that = this;

		console.time("NoName:ScrapTF");

		this.enabled = NoName.Storage.get('scrap:enabled', 0);
		this.lastEntry = NoName.Storage.get('scrap:lastentry', 0);
		this.lastComment = NoName.Storage.get('scrap:lastcomment', 0);

		//Opening multiple tabs will not save you
		NoName.Storage.listen('scrap:enabled', function(oldValue, newValue, url) {
			that.enabled = newValue;
		});

		NoName.Storage.listen('scrap:lastentry', function(oldValue, newValue, url) {
			that.lastEntry = newValue;
		});

		NoName.Storage.listen('scrap:lastcomment', function(oldValue, newValue, url) {
			that.lastComment = newValue;
		});

		console.timeEnd("NoName:ScrapTF");
	},

	isEnabled: function() {
		return !!this.enabled;
	},

	enable: function() {
		var now = new Date().getTime();

		if(this.enabled) {
			return false;
		}

		NoName.Storage.set('scrap:enabled', now);
		this.enabled = now;

		alert('ScrapTF mode enabled');
	},

	//Dunno why you would want to turn it off but here you go
	disable: function() {
		var now = new Date().getTime(),
		difference = (now - this.enabled) / 1000,
		remaining = this.COOLDOWN_DISABLE - difference;

		//No quick escape for you
		if(remaining > 0) {
			alert('Please wait ' + remaining.toFixed(0) + ' seconds to disable ScrapTF mode.');

			return false;
		} else {
			this.enabled = 0;
			NoName.Storage.set('scrap:enabled', '');

			alert('ScrapTF mode disabled');

			return true;
		}
	},

	//Recent development
	//Luckily we aren't a bot or we would be so screwed here!
	canEnterRaffle: function() {
		var now = new Date().getTime(),
		difference = (now - this.lastEntry) / 1000,
		remaining = this.COOLDOWN_RAFFLE - difference;

		if(!this.enabled) {
			return true;
		}

		if(remaining > 0) {
			console.warn('[ScrapTF::canEnterRaffle] Blocking raffle entry');
			alert('Please wait ' + remaining.toFixed(0) + ' seconds to enter this raffle.');
			return false;
		}

		this.lastEntry = now;
		NoName.Storage.set('scrap:lastentry', now);

		return true;
	},

	//Stop spamming pls
	canComment: function() {
		var now = new Date().getTime(),
		difference = (now - this.lastComment) / 1000,
		remaining = this.COOLDOWN_DISABLE - difference;

		if(!this.enabled) {
			return true;
		}

		if(remaining > 0) {
			console.warn('[ScrapTF::canComment] Blocking comment');
			alert('Please don\'t spam');
			return false;
		}

		NoName.Storage.set('scrap:lastcomment', now);
		this.lastComment = now;

		return true;
	},

	removedByStaff: function() {
		return Math.random() < this.REMOVED_CHANCE;
	},
};

window.NoName.Storage = {
	available: false,
	callbacks: {},

	init: function() {
		var that = this;

		if(!localStorage && localStorage.getItem) {
			console.warn('[Storage::init] localStorage not available. Settings will not be saved.');

			return;
		}

		this.available = true;

		window.addEventListener('storage', function(e) {
			that._fire(e);
		});
	},

	get: function(key, defaultValue) {
		if(!this.available) {
			return defaultValue;
		}

		return (typeof localStorage[key] === 'undefined') ? defaultValue : localStorage[key];
	},

	set: function(key, value) {
		if(!this.available) {
			return false;
		}

		var old = localStorage[key];
		localStorage[key] = value;

		this._fire({
			key: key,
			oldValue: old,
			newValue: value,
			url: window.location.href,
		});

		return true;
	},

	_fire: function(e) {
		if(this.callbacks[e.key]) {
			for(var i = 0; i < this.callbacks[e.key].length; i++) {
				this.callbacks[e.key][i](e.oldValue, e.newValue, e.url);
			}
		}
	},

	listen: function(keys, callback) {
		keys = keys.split(' ');

		for(var i = 0; i < keys.length; i++) {
			var key = keys[i];

			this.callbacks[key] = this.callbacks[key] || [];
			this.callbacks[key].push(callback);
		}
	},
};

window.NoName.DB = {
	available: false,

	init: function() {
		var that = this,
		request;

		if(!window.indexedDB) {
			console.warn('[DB::init] indexedDB not available');

			return;
		}

		request = window.indexedDB.open('NoName', 1);

		request.onerror = function(event) {
			console.error('[DB::init] Failed to open indexedDB database. Error: ' + request.errorCode);
		};

		request.onsuccess = function(event) {
			console.info('[DB::init] Database opened');
			this.available = true;
		};

		request.onupgradeneeded = function(event) {
			var db = event.target.result,
			raffles = db.createObjectStore('raffles', {
				keyPath: 'id',
			});

			raffles.createIndex('title', 'title', {
				unique: false,
			});
		};
	},
};

window.NoName.Steam = {
	steamID: '',
	JSON_URL: '',

	init: function() {
		console.time("NoName:Steam");

		this.getSteamID();
		this.JSON_URL = 'http://steamcommunity.com/profiles/' + this.steamID + '/inventory/json/440/2/';

		console.timeEnd("NoName:Steam");
	},

	getSteamID: function() {
		try {
			var result = $('#avatar > a').first().prop('href').match(/https?:\/\/tf2r.com\/user\/(\d+)\.html/);

			this.steamID = result[1];
		} catch(e) {
			console.warn('[Steam::getSteamID] Unable to determine steamID');
		}
	},

	fetchInventoryJSON: function() {
		var defer = jQuery.Deferred();

		if(!this.steamID) {
			defer.reject();

			return defer;
		}

		GM_xmlhttpRequest({
            method: 'GET',
            url: this.JSON_URL,
            onload: function(response) {
            	defer.resolve(response.responseText);
            },
            onerror: function(response) {
				console.error('[Steam::fetchBackpack] Failed to retrieve inventory JSON: ' + response.textStatus);
				defer.reject();
            },
            onprogress: function(response) {
            }
        });

		return defer;
	}
};

//Object that handles parsing and displaying of user's backpack
window.NoName.Backpack = function(options) {
	var that = this;

	console.time("NoName:Backpack");

	this.selectableItems = !!options.selectableItems;
	this.autoRender = !!options.autoRender;
	this.autoLoad = !!options.autoLoad;
	this.tradeableOnly = !!options.tradeableOnly;
	this.loaded = 0;

	this.items = [];
	this.selectedItems = [];
	this.badItems = []; //Selected items that will not be displayed correctly in raffles

	this.levelData = {};
	this.timeout = null;

	this.filters = {}; //Current search filters
	this.oldFilters = {}; //Previous search filters
	this.searchResults = null; //Item indexes that match current filters

	this.$container = options.container;
	this.$selectedContainer = options.selectedContainer;
	this.$info = null;
	this.$displayWarning = null;
	this.$search = null;

	if(!this.$selectedContainer || !this.$selectedContainer.length) {
		console.error('[Backpack] $container does not exist');

		return false;
	}

	function _initElements() {
		that.$container.empty().append($('<ol></ol>'));

		if(that.selectableItems) {
			if(!that.$selectedContainer || !that.$selectedContainer.length) {
				console.error('[Backpack] $selectedContainer must exist for items to be selectable');

				return false;
			}

			that.$selectedContainer.empty().append($('<ol></ol>'));

			that.$container.on('click', 'li', function() {
				if(that.isSelected(this.item)) {
					that.deselect(this);
				} else {
					that.select(this);
				}
			});

			that.$selectedContainer.on('click', 'li', function() {
				that.deselect(this);
			});

			that.$displayWarning = $('<div></div>').addClass('notif lev1').text(
				'Some selected items will not display correctly in your raffle\nConsider listing them in the description.'
			);

			that.$selectedContainer.prepend(that.$displayWarning);
		}

		that.$info = $('<div></div>').addClass('infitem').append([
			$('<strong></strong>').addClass('infname'),
			$('<ul></ul>').addClass('infdesc'),
		]);

		that.$container.append(that.$info);
	}

	function _initFilters() {
		var $qualities = $('<div></div>').addClass('qualities');

		that.$search = $('<input />').prop({
			'placeholder': 'Search',
		}).addClass('search');


		for(var quality in that.qualities) {
			var $quality = $('<input />').prop({
				type: 'checkbox',
				value: quality,
				autocomplete: 'off',
				id: 'q' + quality,
			}),
			$label = $('<label></label>').prop({
				title: that.qualities[quality],
				htmlFor: 'q' + quality,
			}).addClass('q' + quality);

			$qualities.append([
				$quality,
				$label,
			]);
		}

		that.$container.prepend($qualities).prepend(that.$search);
	}

	function _initEvents() {
		that.$container.on('ei:backpackfailed', function() {
			that.$container.addClass('error');
			that.$container.append(
				$('<a></a>').text('Failed to load backpack. Click to retry.')
				.click(function() {
					that.load();
				})
			);
		});

		that.$container.on('click', 'ol', function(event) {
			if(event.target == this) {
				that.loadMore();
			}
		});

		that.$container.on('mouseover', '.item', function(event) {
			_handleHover(event);
		}).on('mouseout', '.item', function() {
			that.$info.hide();
		});

		that.$selectedContainer.on('mouseover', '.item', function(event) {
			_handleHover(event);
		}).on('mouseout', '.item', function() {
			that.$info.hide();
		});

		that.$container.on('change', '.qualities input', function() {
			var qualities = that.$container.find('.qualities input:checked').map(function() {
				return parseInt(this.value);
			}).get();

			if(!qualities.length) {
				delete that.filters.quality;
			} else {
				that.filters.quality = qualities;
			}

			_queueSearch(50);
		});

		that.$search.on('change input', function() {
			if(!this.value) {
				delete that.filters.text;
			} else {
				that.filters.text = this.value;
			}

			_queueSearch(150);
		});
	}

	function _initDragDrop() {
		var source;

		that.$selectedContainer.on('dragstart', '.item', function(e) {
			source = e.target;
		    e.originalEvent.dataTransfer.effectAllowed = 'move';
		    e.originalEvent.dataTransfer.setDragImage(this, 0, 0);
		    e.originalEvent.dataTransfer.setData('text/plain', this.item.name); //Needed for DnD to work at all in firefox;

		    that.$info.hide();
		    $(this).addClass('dragging');
		});

		that.$selectedContainer.on('dragenter', '.item', function(e) {
			var position = source.compareDocumentPosition(this);

			if(position & Node.DOCUMENT_POSITION_DISCONNECTED) {
				return false;
			} else if(position & Node.DOCUMENT_POSITION_PRECEDING) {
				e.target.parentNode.insertBefore(source, e.target);
			} else {
				e.target.parentNode.insertBefore(source, e.target.nextSibling);
		    }
		});

		that.$selectedContainer.on('dragover', '.item', function(e) {
			e.preventDefault();

			return false;
		});

		that.$selectedContainer.on('dragend drop', '.item', function(e) {
		    var oldIndex = that.selectedItems.indexOf(this.item);

		    //Otherwise firefox will happily try to visit "http://Strange Australium Black Box/"
		    e.preventDefault();

		    if(oldIndex < 0) {
		    	console.warn('[Backpack::_initDragDrop] Ignoring unselected item drop');

		    	return true;
		    }

		    var newIndex = that.$selectedContainer.find('.item').index(this);

		    if(newIndex < 0) {
		    	console.warn('[Backpack::_initDragDrop] Ignoring weird situation');

		    	return true;
		    }

		    that.selectedItems.splice(oldIndex, 1);
		    that.selectedItems.splice(newIndex, 0, this.item);
		    $(this).removeClass('dragging');

		    console.debug('[Backpack::_initDragDrop] Moved item ' + this.item.name + ' from ' + oldIndex + ' to ' + newIndex);

		    return false;
		});
	}

	function _queueSearch(delay) {
		if(that.timeout) {
			clearTimeout(that.timeout);
		}

		that.timeout = setTimeout(function() {
			_search();
		}, delay);
	}

	//TODO: Should probably be a public function and accept filters as an argument
	function _search() {
		if(!Object.keys(that.filters).length) {
			that.searchResults = null;
			that.render(true, 0, 50);
			return;
		}

		console.time('NoName:Backpack:_search');

		//Compute any possible optimisations
		var optimisation = (that.searchResults !== null) ? _computeSearchOptimisation(that.filters, that.oldFilters) : false,

		//Create temporary copy of current filters so we can convert the text filter to a regex
		//without breaking future searches that try to compare their text filter with this one
		filters = Object.assign({}, that.filters);

		//Store current filters for later comparison with future searches
		that.oldFilters = Object.assign({}, that.filters);

		//Convert text filter to case insensitive regex
		if(filters.text) {
			filters.text = new RegExp(filters.text, 'i');
		}

		//Handle cases that can be optimised
		switch(optimisation) {
			//Optimisation for AND filters where the new filters are a superset of the old ones
			//Also for OR filters where the new filters are a subset of the old ones
			//In these cases the new results will always be a subset of the old ones, so only the old results need to be checked
			case 'narrowing' :
				var oldResults = that.searchResults,
				newResults = [];
				console.info('[Backpack::_search] Using narrowing optimisation');

				for(var result in oldResults) {
					var index = parseInt(oldResults[result]),
					item = that.items[oldResults[result]];

					if(item.matchesFilters(filters)) {
						newResults.push(index);
					}
				}

				that.searchResults = newResults;

				break;

			//Optimisation for OR filters where the new filters are a superset of the old ones
			//Also for AND filters where the new filters are a subset of the old ones
			//In these cases the new results will always be a superset of the old results, so no need to check the old results again
			case 'widening' :
				var results = that.searchResults;
				console.info('[Backpack::_search] Using widening optimisation');

				for(var item in that.items) {
					item = parseInt(item);

					if(results.indexOf(item) > -1) {
						continue;
					}

					if(that.items[item].matchesFilters(filters)) {
						results.push(item);
					}
				}

				that.searchResults = results.sort(function(a, b) {
					return (a - b);
				});

				break;

			//No optimisations
			default :
				var results = [];

				for(var item in that.items) {
					if(that.items[item].matchesFilters(filters)) {
						results.push(parseInt(item));
					}
				}

				that.searchResults = results;

				break;
		}

		that.render(true, 0, 50);
		console.timeEnd('NoName:Backpack:_search');
	}

	//Determine if the change between old and current search filters can be optimised
	//TODO: handle multiple filter types and filters other than text properly
	function _computeSearchOptimisation(newFilters, oldFilters) {
		var optimisation = false;

		//Text in old filter + no text in new filter = widening
		//Text in both filters + new text is equal to old text = nothing
		//Text in both filters + new text is superset of old text = narrowing
		//Text in both filters + new text is subset of old text = widening
		//Text in new filter + no text in old filter = narrowing
		if(oldFilters.text) {
			if(!newFilters.text) {
				optimisation = 'widening';
			} else if(oldFilters.text === newFilters.text) {
				optimisation = false;
			}else if(newFilters.text.indexOf(oldFilters.text) === 0) {
				optimisation = 'narrowing';
			} else if(oldFilters.text.indexOf(newFilters.text) === 0) {
				optimisation = 'widening';
			}
		} else if(newFilters.text) {
			optimisation = 'narrowing';
		}

		if(oldFilters.quality != newFilters.quality) {
			return false;
		}

		return optimisation;
	}

	function _handleHover(event) {
		var item = event.target.item,
		descriptions = item.getDescriptions(),
		$list = [],

		pos = $(event.target).offset(),
		height = $(event.target).height(),
		width = $(event.target).width();

		//Item name
		that.$info.children('.infname')
			.removeClass()
			.addClass('infname q' + item.getQuality())
			.text(item.getName());

		if(item.getGrade()) {
			that.$info.children('.infname').addClass('hasgrade g' + item.getGrade());
		}

		//Grade colors
		if(item.getGrade()) {
			that.$info.children('.infname');
		}

		//Item level and type
		that.$info.children('.infdesc').empty().append(
			$('<li></li>').text(item.type)
		);

		//Other description strings
		descriptions.forEach(function(description) {
			$list.push(
				$('<li></li>').text(description.value).css({
					color:  (description.color) ? '#' + description.color : '#ffffff',
				})
			);
		});

		that.$info.children('.infdesc').append($list);
		that.$info.show();

		//Position popup properly
		that.$info.css({
			'left': pos.left + (width / 2) - (that.$info.outerWidth() / 2) + 'px',
			'top': pos.top + height + 'px'
		});
	}

	//Parse default item list to get levels of items that do not have a level in the steam inventory json
	function _getLevelData() {
		console.time('NoName:Backpack:_getLevelData');

		//Optimisation
		//Exclude decorated weapons as they never have levels
		that.$container.find('.item:not(.q15)').each(function() {
			//Optimisation
			//Using vanilla javascript here makes this about 10 times faster
			var defindex = this.getAttribute('iid'),
			level =  this.getAttribute('ilevel'),
			quality = this.getAttribute('iqual');

			if(!level) {
				return;
			}

			that.levelData[defindex] = that.levelData[defindex] || {};
			that.levelData[defindex][quality] = that.levelData[defindex][quality] || [];
			that.levelData[defindex][quality].push(level);
		});

		console.timeEnd('NoName:Backpack:_getLevelData');
	}

	//Populate items array with item objects created from parsed data
	function _populateItems(items) {
		for(var item in items) {
			item = items[item];

			that.items.push(new NoName.Item(item, that.levelData));
		}
	}

	//Use a webworker to parse the json and clean up data
	//Doing this on the main thread causes noticable lag
	this.parseJSON = function(json) {
		console.time('NoName:Backpack:parseJSON');

		var that = this,
		defer = jQuery.Deferred(),

		//Create a blob from the below workerParse function to allow its use in the worker
		work = URL.createObjectURL(
			new Blob([
				'(',
		   		this.workerParse.toString(),
		   		')()'
			], {
				type: 'application/javascript'
			})
		),

		//Create worker
	    worker = new Worker(work);

		//Listen for worker response
	    worker.addEventListener('message', function(e) {
	    	if(e.data.success) {
	    		console.info('[Backpack::parseJSON] Backpack parsed');
	    		_populateItems(e.data.items);

				console.timeEnd('NoName:Backpack:parseJSON');
	    		defer.resolve();
	    	} else {
    			console.error('[Backpack::parseJSON] Failed to parse backpack: ' + e.data.error);

				console.timeEnd('NoName:Backpack:parseJSON');
    			defer.reject();
	    	}
	    }, false);

	    //Send the worker the json to parse
	    worker.postMessage(json);
	    URL.revokeObjectURL(work);

	    return defer;
	};

	//Used by web worker to parse the json and then restructure the parsed data
	this.workerParse = function() {
		self.addEventListener('message', function(event) {
			try {
				var data = JSON.parse(event.data),
				items = parseItems(data);

				if(items) {
					self.postMessage({success: true, items: items});
				} else {
					throw new Error('Item parsing failed');
				}

			} catch (e) {
				console.error('[Steam::workerParse] Error while parsing JSON : ' + e);
				self.postMessage({success: false, error: e.toString()});
			}
		});

		//Checks the parsed json is a valid response
		//Merges item and description arrays into a single item array
		//Removes unneeded item data
		function parseItems(data) {
			var items = data.rgInventory,
			descriptions = data.rgDescriptions,
			parsedItems = [];

			if(!data.success) {
				console.error('[Backpack::workerParse] Success property is false');

				return false;
			}

			if(!items) {
				console.error('[Backpack::workerParse] Inventory array missing');

				return false;
			}

			if(!descriptions) {
				console.error('[Backpack::workerParse] Descriptions array missing');

				return false;
			}

			for(var item in items) {
				item = items[item];

				var classInstanceId = item.classid + '_' + item.instanceid,
				description = descriptions[classInstanceId];

				parsedItems.push(parseItem(item, description));
			}

			parsedItems.sort(function(item1, item2) {
				return item1.position - item2.position;
			});

			return parsedItems;
		}

		//Parses a single item
		//Extracts level and series data where possible
		//Loops over descriptions to determine which ones to item.descriptions
		//Moved to worker as it is very slow
		function parseItem(item, description) {
			var level = description.type.match(/.*Level (\d+).*/),
			series = description.name.match(/.*Series #(\d+).*/);

			level = (level) ? level[1] : 0;
			series = (series) ? series[1] : 0;

			var parsedItem = {
				id: item.id,
				position: item.pos,
				defindex: description.app_data.def_index,
				quality: description.app_data.quality,
				name: description.name,
				type: description.type,
				level: level,
				series: series,
				tradable: description.tradable,
				descriptions: [],
				tags: [],
				thumbnail: description.icon_url + '/128fx128f', //Reduce image size
				image: description.icon_url_large,
				classes: [],
			};

			parseDescriptions(parsedItem, description.descriptions || []);
			parseTags(parsedItem, description.tags || []);

			return parsedItem;
		}

		//Loops over the description strings for an item and returns the ones we care about
		//Moved to the worker as it is slow enough to cause noticeable ui lag
		function parseDescriptions(item, descriptions) {
			descriptions.forEach(function(description) {

				//Basic killstreak
				if(description.value === 'Killstreaks Active') {
					item.descriptions.push(description);
					return;
				}

				//Specialized killstreak, Professional killstreak,
				//Gifts, Paint, Crafted, Unusual effects, Stat-clock, Festivised
				if(!description.value.indexOf('Sheen: ') ||
                   !description.value.indexOf('Killstreaker: ') ||
                   !description.value.indexOf('\nGift from: ') ||
                   !description.value.indexOf('Paint Color: ') ||
                   !description.value.indexOf('Crafted by ') ||
                   !description.value.indexOf('Festivized') ||
                   !description.value.indexOf('Strange Stat Clock Attached') ||
                   !description.value.indexOf('★ Unusual Effect')) {
					item.descriptions.push(description);
					return;
				}

				//Spells
				if(!description.value.indexOf('Halloween: ')) {
					description.value = description.value.replace('(spell only active during event)', '');

					item.descriptions.push(description);
					return;
				}

				//Custom descriptions, strange parts
				if(description.value.match(/^\'\'.*\'\'$/) ||
                   description.value.match(/^\(.*:.*\)$/)) {
					item.descriptions.push(description);
					return;
				}

				//Collection grades
				if((grade = description.value.match(/^(\w+) Grade (.*)$/)) && description.color) {
					item.descriptions.push(description);
					return;
				}
			});

			return item;
		}

		//Parse item tags that we care about
		//Not yet implemented
		function parseTags(item, tags) {
			tags.forEach(function(tag) {
				switch(tag.category) {
					case 'Rarity':
						item.grade = tag.name.toLowerCase();
						return;

					case 'Class':
						item.classes.push(tag.name.toLowerCase());
						return;

					case 'Exterior':
						item.wear = tag.name;
						return;

					case 'Type' :
						item.slot = tag.name;
						return;
				}
			});

			return item;
		}
	};

	_getLevelData();
	_initElements();
	_initFilters();
	_initEvents();
	_initDragDrop();

	if(this.autoLoad) {
		this.load();
	}

	console.timeEnd("NoName:Backpack");
};

window.NoName.Backpack.prototype = {
	qualities: {
		0: 'Normal',
		1: 'Genuine',
		3: 'Vintage',
		5: 'Unusual',
		6: 'Unique',
		11: 'Strange',
		13: 'Haunted',
		14: 'Collector\'s',
		15: 'Decorated Weapon',
	},

	loadMore: function() {
		this.render(false, this.loaded, this.loaded + 300);
	},

	//Renders backpack items, within an optional range and optionally emptying the parent element
	//Renders search results if there are any, otherwise all items
	render: function(empty, fromPos, toPos) {
		console.time('NoName:Backpack:render');

		var that = this,
		items = document.createDocumentFragment();

		if(empty) {
			this.$container.children('ol').empty();
		}

		if(this.searchResults !== null) {
			if(!toPos || toPos > this.searchResults.length) {
				this.$container.removeClass('minimised');
				toPos = this.items.length;
			} else {
				this.$container.addClass('minimised');
			}

			this.searchResults.slice(fromPos, toPos).forEach(function(item, index) {
				item = that.items[item];
				items.appendChild(that.renderItem(item));
			});
		} else {
			if(!toPos || toPos > this.items.length) {
				this.$container.removeClass('minimised');
				toPos = this.items.length;
			} else {
				this.$container.addClass('minimised');
			}

			this.items.slice(fromPos, toPos).forEach(function(item, index) {
				items.appendChild(that.renderItem(item));
			});
		}

		this.$container.children('ol').append(items);
		this.loaded = toPos;

		console.timeEnd('NoName:Backpack:render');
	},

	//Renders a single item, greying it out if it is selected
	//Using standard javascript, need all the performance I can get here
	renderItem: function(item) {
		var element = document.createElement('li');
		element.className = 'item q' + item.getQuality();

		if(item.getGrade()) {
			element.className += ' hasgrade g' + item.getGrade();
		}

		if(this.selectedItems.indexOf(item) > -1) {
			element.className += ' selected';
		}

		element.item = item;
		element.style.backgroundImage  = 'url(' + item.getThumbnail() + ')';

		return element;
	},

	load: function(force) {
		var that = this,
		jsonLoad = NoName.Steam.fetchInventoryJSON();

		this.$container.trigger('ei:backpackloading');
		this.$container.addClass('loading');

		jsonLoad.done(function(json) {
			that.parseJSON(json).done(function() {
				that.$container.trigger('ei:backpackloaded');

				if(that.tradeableOnly) {
					that.items = that.items.filter(function(item) {
						return item.isTradable();
					});
				}

	    		if(that.autoRender) {
	    			that.render(true, 0, 50);
	    		}
			}).fail(function() {
				that.$container.trigger('ei:backpackfailed');
			}).always(function() {
				that.$container.removeClass('loading');
			});
		}).fail(function() {
			console.error('[Backpack::load] Failed to load backpack');

			this.$container.trigger('ei:backpackfailed');
			that.$container.removeClass('loading');
		});
	},

	select: function(element) {
		var item = element.item;

		if($(element).parents().index(this.$container) === -1) {
			console.error('[Backpack::select] item is not a descendant of item container');

			return false;
		}

		if(this.items.indexOf(item) < 0) {
			console.error('[Backpack::select] Item does not exist in backpack');

			return false;
		}

		//Clone item element and add to selected item list
		var clone = element.cloneNode(false);
		clone.item = item;
		clone.draggable = true;

		this.selectedItems.push(item);
		this.$selectedContainer.children('ol').append(clone);

		//Update existing element to show item has been selected
		$(element).addClass('selected');

		//Show warning if this item won't be displayed correctly in the raffle
		if(this.selectableItems && !item.willDisplayCorrectly()) {
			console.warn('[Backpack::select] Selected item will not be displayed correctly');

			this.badItems.push(item);
			this.$displayWarning.show();
		}

		console.debug('[Backpack::deselect] Selected item ' + item.name);

		return true;
	},

	//Deselect an item, removing it from the list of selected items and allowing it to be selected again
	deselect: function(element) {
		var item = element.item,
		index = this.selectedItems.indexOf(item);

		if(index < 0) {
			console.error('[Backpack::deselect] Item does not exist in backpack');

			return false;
		}

		//Remove from selected array
		this.selectedItems.splice(index, 1);

		//If the passed element is in the selected list remove it
		//Otherwise find it and then remove it
		//Also restore the appearance of the element in the main list
		if($(element).parents().index(this.$selectedContainer) > -1) {
			//Remove element from selected list
			$(element).remove();
			this.$info.hide();

			//Get index of current item in item list or search results (if search results are currently being shown)
			index = this.items.indexOf(item);
			if(this.searchResults) {
				index = this.searchResults.indexOf(index);
			}

			//Use above index to restore item in the list of selectable items
			this.$container.children('ol').children().eq(index).removeClass('selected');
		} else {
			//Use above index to remove element from selected list
			this.$selectedContainer.children('ol').children().eq(index).remove();

			//Restore appearance of main element
			$(element).removeClass('selected');
		}

		//Remove from bad items list if it exists
		if(this.selectableItems && this.badItems.indexOf(item) > -1) {
			this.badItems.splice(this.badItems.indexOf(item), 1);
		}

		//If bad item list is empty remove warning
		if(this.selectableItems && !this.badItems.length) {
			this.$displayWarning.hide();
		}

		console.debug('[Backpack::deselect] Deselected item ' + item.name);

		return true;
	},

	isSelected: function(item) {
		return !!(this.selectedItems.indexOf(item) > -1);
	},

	getSelected: function() {
		return this.selectedItems;
	}
};

//Object that represents a single item
window.NoName.Item = function(data, levelData) {
	var that = this;

	//General stuff
	this.id = data.id;
	this.defindex = parseInt(data.defindex);

	this.quality = parseInt(data.quality);
	this.tradable = !!data.tradable;

	this.name = data.name || '';
	this.type = data.type || '';
	this.level = data.level;
	this.grade = data.grade || null;
	this.classes = data.classes || [];

	//Fallback to level data found in the default item list if the json api didn't give us one
	//This should only be needed for stranges, as they don't show levels in the steam inventory
	//Using this data is a guess, but it will usually be correct unless the user has multiple copies of the same strange
	//which differ in a noticeable way such as parts
	if(data.level) {
		this.level = data.level;
	} else if(levelData && levelData[this.defindex] && levelData[this.defindex][this.quality]) {
		this.level = levelData[this.defindex][this.quality];
	} else {
		this.level = 1; //The site uses 1 for items that don't have a level
	}

	this.series = data.series || 0;

	this.thumbnail = this.IMAGE_URL + data.thumbnail;
	this.image = this.IMAGE_URL + data.image;

	this.position = data.position;
	this.descriptions = data.descriptions;
	this.tags = data.tags;
};

window.NoName.Item.prototype = {
	IMAGE_URL: 'https://steamcommunity-a.akamaihd.net/economy/image/',

	EXPORT_NAME: 1,
	EXPORT_DESCRIPTION: 2,
	EXPORT_UNUSUAL_EFFECT: 3,
	EXPORT_KILLSTREAK_SHEEN: 4,
	EXPORT_KILLSTREAK_EFFECT: 5,
	EXPORT_STRANGE_PARTS: 6,
	EXPORT_GIFTER: 7,
	EXPORT_CRAFTER: 8,
	EXPORT_CRAFT_NUMBER: 9,
	EXPORT_WEAR: 10,
	EXPORT_AUSTRALIUM: 11,
	EXPORT_GRADE: 12,

	//Newer taunts
	//Festivizers
	//Smissmass gifts
	badIndexes: [30671, 30618, 5838, 5839, 1162],

	getDefIndex: function() {
		return this.defindex;
	},

	getName: function() {
		return this.name;
	},

	getThumbnail: function() {
		return this.thumbnail;
	},

	getQuality: function() {
		return this.quality;
	},

	getGrade: function() {
		return this.grade;
	},

	getLevel: function() {
		return this.level;
	},

	getSeries: function() {
		return this.series;
	},

	getPosition: function() {
		return this.position;
	},

	isTradable: function() {
		return this.tradable;
	},

	//Checks whether the current item is likely to be displayed correctly
	//Many newer items don't appear correctly due to missing schema information:
	//Appearing as stock weapons, having unlocalised names, or having no image and name at all
	//Returning true is not a guarantee of correct display, but should be a good enough guess for most items
	willDisplayCorrectly: function() {
		//Skins show as stock or nothing at all
		if(this.defindex >= 15000 && this.defindex < 16000) {
			return false;
		}

		//Tough break cosmetics and anything valve adds in the future in this range
		if(this.defindex > 30742) {
			return false;
		}

		//Other known bad defindexes
		if(this.badIndexes.indexOf(this.defindex) > -1) {
			return false;
		}

		return true;
	},

	//Returns extended information from the dictionary, using the items schema name
	//Used for skins that don't display correctly but still include a name
	//As well as some incorrectly named items
	getExtendedInformation: function(schemaName) {
		var prefix = '',
		information = null,
		qualities = /^(Strange|Vintage|Genuine|Haunted|Unusual) /,
		match = schemaName.match(qualities);

		if(match) {
			prefix = match[0];
			schemaName = schemaName.substring(match[0].length);
		}

		if(!window.itemDictionary) {
			return false;
		}

		if(!window.itemDictionary[schemaName]) {
			return false;
		}

		information = window.itemDictionary[schemaName];
		information.name = prefix + information.name;

		if(information.image) {
			information.image = this.IMAGE_URL + information.image;
		}

		return information;
	},

	getDescriptions: function() {
		return this.descriptions;
	},

	//Determines if item matches search filters
	matchesFilters: function(filters) {
		if(filters.quality) {
			if(filters.quality.length) {
				if(filters.quality.indexOf(this.quality) === -1) {
					return false;
				}
			} else if(filters.quality != this.quality) {
				return false;
			}
		}


		if(filters.text) {
			if(!filters.text.test(this.name)) {
				return false;
			}
		}

		return true;
	},

	exportItem: function() {

	},
};

window.NoName.Item.importItem = function(data) {
	var item = new window.NoName.Item();

	return item;
};

window.NoName.Storage.init();
window.NoName.exportOverrides(); //Export override functions
window.NoName.UI.addStyles(); //Add CSS early

$(document).ready(function() {
	//Nu iframes pls
	if(window.top == window.self) {
		//Export override functions again to make sure
		window.NoName.exportOverrides();

		console.time("NoName");
		//Lets get this party started
		window.NoName.init();
		console.timeEnd("NoName");

		//Unhide page content
		$('#content .indent').css('opacity', '1');
	}
});