#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#include <advanced_motd>
#include <SteamWorks>

#define PLUGIN_VERSION		"2.11.1-skial-steamworks"
#define BACKPACK_TF_URL		"http://backpack.tf/api/IGetPrices/v3/"
#define ITEM_EARBUDS		143
#define ITEM_REFINED		5002
#define ITEM_KEY			5021
#define ITEM_CRATE			5022
#define ITEM_SALVAGED_CRATE	5068
#define ITEM_HAUNTED_SCRAP	267
#define ITEM_HEADTAKER		266
#define QUALITY_UNIQUE		"6"
#define QUALITY_UNUSUAL		"5"
#define NOTIFICATION_SOUND	"replay/downloadcomplete.wav"

public Plugin:myinfo = {
	name        = "[TF2] backpack.tf Price Check",
	author      = "Bottiger, Dr. McKay",
	description = "Provides a price check command for use with backpack.tf",
	version     = PLUGIN_VERSION,
	url         = "http://www.doctormckay.com"
};

new lastCacheTime;
new cacheTime;
new Handle:backpackTFPricelist;

new Handle:qualityNameTrie;
new Handle:unusualNameTrie;

new Handle:cvarBPCommand;
new Handle:cvarDisplayUpdateNotification;
new Handle:cvarDisplayChangedPrices;
new Handle:cvarHudXPos;
new Handle:cvarHudYPos;
new Handle:cvarHudRed;
new Handle:cvarHudGreen;
new Handle:cvarHudBlue;
new Handle:cvarHudHoldTime;
new Handle:cvarMenuHoldTime;
new Handle:cvarAPIKey;
new Handle:cvarTag;

new Handle:hudText;
new Handle:sv_tags;

new Float:budsToKeys;
new Float:keysToRef;
new Float:refToUsd;

#define UPDATE_FILE		"backpack-tf.txt"
#define CONVAR_PREFIX	"backpack_tf"

//#include "mckayupdater.sp"

public OnPluginStart() {
	cvarBPCommand = CreateConVar("backpack_tf_bp_command", "1", "Enables the !bp command for use with backpack.tf");
	cvarDisplayUpdateNotification = CreateConVar("backpack_tf_display_update_notification", "0", "Display a notification to clients when the cached price list has been updated?");
	cvarDisplayChangedPrices = CreateConVar("backpack_tf_display_changed_prices", "1", "If backpack_tf_display_update_notification is set to 1, display all prices that changed since the last update?");
	cvarHudXPos = CreateConVar("backpack_tf_update_notification_x_pos", "-1.0", "X position for HUD text from 0.0 to 1.0, -1.0 = center", _, true, -1.0, true, 1.0);
	cvarHudYPos = CreateConVar("backpack_tf_update_notification_y_pos", "0.1", "Y position for HUD text from 0.0 to 1.0, -1.0 = center", _, true, -1.0, true, 1.0);
	cvarHudRed = CreateConVar("backpack_tf_update_notification_red", "0", "Red value of HUD text", _, true, 0.0, true, 255.0);
	cvarHudGreen = CreateConVar("backpack_tf_update_notification_green", "255", "Green value of HUD text", _, true, 0.0, true, 255.0);
	cvarHudBlue = CreateConVar("backpack_tf_update_notification_blue", "0", "Blue value of HUD text", _, true, 0.0, true, 255.0);
	cvarHudHoldTime = CreateConVar("backpack_tf_update_notification_message_time", "5", "Seconds to keep each message in the update ticker on the screen", _, true, 0.0);
	cvarMenuHoldTime = CreateConVar("backpack_tf_menu_open_time", "0", "Time to keep the price panel open for, 0 = forever");
	cvarAPIKey = CreateConVar("backpack_tf_api_key", "", "API key obtained at http://backpack.tf/api/register/", FCVAR_PROTECTED);
	cvarTag = CreateConVar("backpack_tf_add_tag", "1", "If 1, adds the backpack.tf tag to your server's sv_tags, which is required to be listed on http://backpack.tf/servers", _, true, 0.0, true, 1.0);
	AutoExecConfig();
	
	LoadTranslations("backpack-tf.phrases");
	
	sv_tags = FindConVar("sv_tags");
	
	RegConsoleCmd("sm_bp", Command_Backpack, "Usage: sm_bp <player>");
	RegConsoleCmd("sm_backpack", Command_Backpack, "Usage: sm_backpack <player>");
	
	RegConsoleCmd("sm_pc", Command_PriceCheck, "Usage: sm_pc <item>");
	RegConsoleCmd("sm_pricecheck", Command_PriceCheck, "Usage: sm_pricecheck <item>");
	
	RegAdminCmd("sm_updateprices", Command_UpdatePrices, ADMFLAG_ROOT, "Updates backpack.tf prices");
	
	qualityNameTrie = CreateTrie();
	SetTrieString(qualityNameTrie, "0", "Normal");
	SetTrieString(qualityNameTrie, "1", "Genuine");
	SetTrieString(qualityNameTrie, "2", "rarity2");
	SetTrieString(qualityNameTrie, "3", "Vintage");
	SetTrieString(qualityNameTrie, "4", "rarity3");
	SetTrieString(qualityNameTrie, "5", "Unusual");
	SetTrieString(qualityNameTrie, "6", "Unique");
	SetTrieString(qualityNameTrie, "7", "Community");
	SetTrieString(qualityNameTrie, "8", "Valve");
	SetTrieString(qualityNameTrie, "9", "Self-Made");
	SetTrieString(qualityNameTrie, "10", "Customized");
	SetTrieString(qualityNameTrie, "11", "Strange");
	SetTrieString(qualityNameTrie, "12", "Completed");
	SetTrieString(qualityNameTrie, "13", "Haunted");
	SetTrieString(qualityNameTrie, "14", "Collector's");
	SetTrieString(qualityNameTrie, "300", "Uncraftable Vintage"); // custom for backpack.tf
	SetTrieString(qualityNameTrie, "600", "Uncraftable"); // custom for backpack.tf
	SetTrieString(qualityNameTrie, "1100", "Uncraftable Strange"); // custom for backpack.tf
	SetTrieString(qualityNameTrie, "1300", "Uncraftable Haunted"); // custom for backpack.tf
	
	unusualNameTrie = CreateTrie();
	// Original effects
	SetTrieString(unusualNameTrie, "6", "Green Confetti");
	SetTrieString(unusualNameTrie, "7", "Purple Confetti");
	SetTrieString(unusualNameTrie, "8", "Haunted Ghosts");
	SetTrieString(unusualNameTrie, "9", "Green Energy");
	SetTrieString(unusualNameTrie, "10", "Purple Energy");
	SetTrieString(unusualNameTrie, "11", "Circling TF Logo");
	SetTrieString(unusualNameTrie, "12", "Massed Flies");
	SetTrieString(unusualNameTrie, "13", "Burning Flames");
	SetTrieString(unusualNameTrie, "14", "Scorching Flames");
	SetTrieString(unusualNameTrie, "15", "Searing Plasma");
	SetTrieString(unusualNameTrie, "16", "Vivid Plasma");
	SetTrieString(unusualNameTrie, "17", "Sunbeams");
	SetTrieString(unusualNameTrie, "18", "Circling Peace Sign");
	SetTrieString(unusualNameTrie, "19", "Circling Heart");
	// Batch 2
	SetTrieString(unusualNameTrie, "29", "Stormy Storm");
	SetTrieString(unusualNameTrie, "30", "Blizzardy Storm");
	SetTrieString(unusualNameTrie, "31", "Nuts n' Bolts");
	SetTrieString(unusualNameTrie, "32", "Orbiting Planets");
	SetTrieString(unusualNameTrie, "33", "Orbiting Fire");
	SetTrieString(unusualNameTrie, "34", "Bubbling");
	SetTrieString(unusualNameTrie, "35", "Smoking");
	SetTrieString(unusualNameTrie, "36", "Steaming");
	// Halloween
	SetTrieString(unusualNameTrie, "37", "Flaming Lantern");
	SetTrieString(unusualNameTrie, "38", "Cloudy Moon");
	SetTrieString(unusualNameTrie, "39", "Cauldron Bubbles");
	SetTrieString(unusualNameTrie, "40", "Eerie Orbiting Fire");
	SetTrieString(unusualNameTrie, "43", "Knifestorm");
	SetTrieString(unusualNameTrie, "44", "Misty Skull");
	SetTrieString(unusualNameTrie, "45", "Harvest Moon");
	SetTrieString(unusualNameTrie, "46", "It's A Secret To Everybody");
	SetTrieString(unusualNameTrie, "47", "Stormy 13th Hour");
	// Batch 3
	SetTrieString(unusualNameTrie, "56", "Kill-a-Watt");
	SetTrieString(unusualNameTrie, "57", "Terror-Watt");
	SetTrieString(unusualNameTrie, "58", "Cloud 9");
	SetTrieString(unusualNameTrie, "59", "Aces High");
	SetTrieString(unusualNameTrie, "60", "Dead Presidents");
	SetTrieString(unusualNameTrie, "61", "Miami Nights");
	SetTrieString(unusualNameTrie, "62", "Disco Beat Down");
	// Robo-effects
	SetTrieString(unusualNameTrie, "63", "Phosphorous");
	SetTrieString(unusualNameTrie, "64", "Sulphurous");
	SetTrieString(unusualNameTrie, "65", "Memory Leak");
	SetTrieString(unusualNameTrie, "66", "Overclocked");
	SetTrieString(unusualNameTrie, "67", "Electrostatic");
	SetTrieString(unusualNameTrie, "68", "Power Surge");
	SetTrieString(unusualNameTrie, "69", "Anti-Freeze");
	SetTrieString(unusualNameTrie, "70", "Time Warp");
	SetTrieString(unusualNameTrie, "71", "Green Black Hole");
	SetTrieString(unusualNameTrie, "72", "Roboactive");
	// Halloween 2013
	SetTrieString(unusualNameTrie, "73", "Arcana");
	SetTrieString(unusualNameTrie, "74", "Spellbound");
	SetTrieString(unusualNameTrie, "75", "Chiroptera Venenata");
	SetTrieString(unusualNameTrie, "76", "Poisoned Shadows");
	SetTrieString(unusualNameTrie, "77", "Something Burning This Way Comes");
	SetTrieString(unusualNameTrie, "78", "Hellfire");
	SetTrieString(unusualNameTrie, "79", "Darkblaze");
	SetTrieString(unusualNameTrie, "80", "Demonflame");
	// Halloween 2014
	SetTrieString(unusualNameTrie, "81", "Bonzo The All-Gnawing");
	SetTrieString(unusualNameTrie, "82", "Amaranthine");
	SetTrieString(unusualNameTrie, "83", "Stare From Beyond");
	SetTrieString(unusualNameTrie, "84", "The Ooze");
	SetTrieString(unusualNameTrie, "85", "Ghastly Ghosts Jr");
	SetTrieString(unusualNameTrie, "86", "Haunted Phantasm Jr");
	// EOTL
	SetTrieString(unusualNameTrie, "87", "Frostbite");
	SetTrieString(unusualNameTrie, "88", "Molten Mallard");
	SetTrieString(unusualNameTrie, "89", "Morning Glory");
	SetTrieString(unusualNameTrie, "90", "Death at Dusk");
	// Taunt effects
	SetTrieString(unusualNameTrie, "3001", "Showstopper");
	SetTrieString(unusualNameTrie, "3002", "Showstopper");
	SetTrieString(unusualNameTrie, "3003", "Holy Grail");
	SetTrieString(unusualNameTrie, "3004", "'72");
	SetTrieString(unusualNameTrie, "3005", "Fountain of Delight");
	SetTrieString(unusualNameTrie, "3006", "Screaming Tiger");
	SetTrieString(unusualNameTrie, "3007", "Skill Gotten Gains");
	SetTrieString(unusualNameTrie, "3008", "Midnight Whirlwind");
	SetTrieString(unusualNameTrie, "3009", "Silver Cyclone");
	SetTrieString(unusualNameTrie, "3010", "Mega Strike");
	// Halloween 2014 taunt effects
	SetTrieString(unusualNameTrie, "3011", "Haunted Phantasm");
	SetTrieString(unusualNameTrie, "3012", "Ghastly Ghosts");
	
	hudText = CreateHudSynchronizer();
}

public OnConfigsExecuted() {
	CreateTimer(2.0, Timer_AddTag); // Let everything load first
}

public Action:Timer_AddTag(Handle:timer) {
	if(!GetConVarBool(cvarTag)) {
		return;
	}
	decl String:value[512];
	GetConVarString(sv_tags, value, sizeof(value));
	TrimString(value);
	if(strlen(value) == 0) {
		SetConVarString(sv_tags, "backpack.tf");
		return;
	}
	decl String:tags[64][64];
	new total = ExplodeString(value, ",", tags, sizeof(tags), sizeof(tags[]));
	for(new i = 0; i < total; i++) {
		if(StrEqual(tags[i], "backpack.tf")) {
			return; // Tag found, nothing to do here
		}
	}
	StrCat(value, sizeof(value), ",backpack.tf");
	SetConVarString(sv_tags, value);
}

public OnMapStart() {
	PrecacheSound(NOTIFICATION_SOUND);
}

public Steam_FullyLoaded() {
	CreateTimer(1.0, Timer_Update); // In case of late-loads
}

GetCachedPricesAge() {
	decl String:path[PLATFORM_MAX_PATH];
	BuildPath(Path_SM, path, sizeof(path), "data/backpack-tf.txt");
	if(!FileExists(path)) {
		return -1;
	}
	new Handle:kv = CreateKeyValues("Response");
	if(!FileToKeyValues(kv, path)) {
		CloseHandle(kv);
		return -1;
	}
	new offset = KvGetNum(kv, "time_offset", 1337); // The actual offset can be positive, negative, or zero, so we'll just use 1337 as a default since that's unlikely
	new time = KvGetNum(kv, "current_time");
	CloseHandle(kv);
	if(offset == 1337 || time == 0) {
		return -1;
	}
	return GetTime() - time;
}

public Action:Timer_Update(Handle:timer) {
	new age = GetCachedPricesAge();
	if(age != -1 && age < 900) { // 15 minutes
		LogMessage("Locally saved pricing data is %d minutes old, bypassing backpack.tf query", age / 60);
		if(backpackTFPricelist != INVALID_HANDLE) {
			CloseHandle(backpackTFPricelist);
		}
		decl String:path[PLATFORM_MAX_PATH];
		BuildPath(Path_SM, path, sizeof(path), "data/backpack-tf.txt");
		backpackTFPricelist = CreateKeyValues("Response");
		FileToKeyValues(backpackTFPricelist, path);
		
		budsToKeys = GetConversion(ITEM_EARBUDS);
		keysToRef = GetConversion(ITEM_KEY);
		KvRewind(backpackTFPricelist);
		refToUsd = KvGetFloat(backpackTFPricelist, "refined_usd_value");
		
		CreateTimer(float(3600 - age), Timer_Update);
		return;
	}
	
	decl String:key[32];
	GetConVarString(cvarAPIKey, key, sizeof(key));
	if(strlen(key) == 0) {
		LogError("No API key set. Fill in your API key and reload the plugin.");
		return;
	}
	Handle request = SteamWorks_CreateHTTPRequest(k_EHTTPMethodGET, BACKPACK_TF_URL);
	SteamWorks_SetHTTPRequestGetOrPostParameter(request, "key", key);
	SteamWorks_SetHTTPRequestGetOrPostParameter(request, "format", "vdf");
	SteamWorks_SetHTTPRequestGetOrPostParameter(request, "names", "1");

	SteamWorks_SetHTTPCallbacks(request, OnBackpackTFComplete);
	SteamWorks_SendHTTPRequest(request);
}

public OnBackpackTFComplete(Handle request, bool failure, bool successful, EHTTPStatusCode status) {
	if(status != k_EHTTPStatusCode200OK || !successful) {
		if(status == k_EHTTPStatusCode400BadRequest) {
			LogError("backpack.tf API failed: You have not set an API key");
			CloseHandle(request);
			CreateTimer(600.0, Timer_Update); // Set this for 10 minutes instead of 1 minute
			return;
		} else if(status == k_EHTTPStatusCode403Forbidden) {
			LogError("backpack.tf API failed: Your API key is invalid");
			CloseHandle(request);
			CreateTimer(600.0, Timer_Update); // Set this for 10 minutes instead of 1 minute
			return;
		} else if(status == k_EHTTPStatusCode412PreconditionFailed) {
			decl String:retry[16];
			SteamWorks_GetHTTPResponseHeaderValue(request, "Retry-After", retry, sizeof(retry));
			LogError("backpack.tf API failed: We are being rate-limited by backpack.tf, next request allowed in %s seconds", retry);
		} else if(status >= k_EHTTPStatusCode500InternalServerError) {
			LogError("backpack.tf API failed: An internal server error occurred");
		} else if(status == k_EHTTPStatusCode200OK && !successful) {
			LogError("backpack.tf API failed: backpack.tf returned an OK response but no data");
		} else if(status != k_EHTTPStatusCodeInvalid) {
			LogError("backpack.tf API failed: Unknown error (status code %d)", _:status);
		} else {
			LogError("backpack.tf API failed: Unable to connect to server or server returned no data");
		}
		CloseHandle(request);
		CreateTimer(60.0, Timer_Update); // try again!
		return;
	}
	decl String:path[256];
	BuildPath(Path_SM, path, sizeof(path), "data/backpack-tf.txt");
	
	SteamWorks_WriteHTTPResponseBodyToFile(request, path);
	CloseHandle(request);
	LogMessage("backpack.tf price list successfully downloaded!");
	
	CreateTimer(3600.0, Timer_Update);
	
	if(backpackTFPricelist != INVALID_HANDLE) {
		CloseHandle(backpackTFPricelist);
	}
	backpackTFPricelist = CreateKeyValues("Response");
	FileToKeyValues(backpackTFPricelist, path);
	lastCacheTime = cacheTime;
	cacheTime = KvGetNum(backpackTFPricelist, "current_time");
	
	new offset = GetTime() - cacheTime;
	KvSetNum(backpackTFPricelist, "time_offset", offset);
	KeyValuesToFile(backpackTFPricelist, path);
	
	budsToKeys = GetConversion(ITEM_EARBUDS);
	keysToRef = GetConversion(ITEM_KEY);
	KvRewind(backpackTFPricelist);
	refToUsd = KvGetFloat(backpackTFPricelist, "refined_usd_value");
	
	if(!GetConVarBool(cvarDisplayUpdateNotification)) {
		return;
	}
	
	if(lastCacheTime == 0) { // first download
		new Handle:array = CreateArray(128);
		PushArrayString(array, "#Type_command");
		SetHudTextParams(GetConVarFloat(cvarHudXPos), GetConVarFloat(cvarHudYPos), GetConVarFloat(cvarHudHoldTime), GetConVarInt(cvarHudRed), GetConVarInt(cvarHudGreen), GetConVarInt(cvarHudBlue), 255);
		for(new i = 1; i <= MaxClients; i++) {
			if(!IsClientInGame(i)) {
				continue;
			}
			ShowSyncHudText(i, hudText, "%t", "Price list updated");
			EmitSoundToClient(i, NOTIFICATION_SOUND);
		}
		CreateTimer(GetConVarFloat(cvarHudHoldTime), Timer_DisplayHudText, array, TIMER_REPEAT);
		return;
	}
	
	PrepPriceKv();
	KvGotoFirstSubKey(backpackTFPricelist);
	new bool:isNegative = false;
	new lastUpdate, Float:valueOld, Float:valueOldHigh, Float:value, Float:valueHigh, Float:difference;
	decl String:defindex[16], String:qualityIndex[32], String:quality[32], String:name[64], String:message[128], String:currency[32], String:currencyOld[32], String:oldPrice[64], String:newPrice[64];
	new Handle:array = CreateArray(128);
	PushArrayString(array, "#Type_command");
	if(GetConVarBool(cvarDisplayChangedPrices)) {
		do {
			// loop through items
			KvGetSectionName(backpackTFPricelist, defindex, sizeof(defindex));
			if(StringToInt(defindex) == ITEM_REFINED) {
				continue; // Skip over refined price changes
			}
			KvGotoFirstSubKey(backpackTFPricelist);
			do {
				// loop through qualities
				KvGetSectionName(backpackTFPricelist, qualityIndex, sizeof(qualityIndex));
				if(StrEqual(qualityIndex, "item_info"))  {
					KvGetString(backpackTFPricelist, "item_name", name, sizeof(name));
					continue;
				}
				KvGotoFirstSubKey(backpackTFPricelist);
				do {
					// loop through instances (series #s, effects)
					lastUpdate = KvGetNum(backpackTFPricelist, "last_change");
					if(lastUpdate == 0 || lastUpdate < lastCacheTime) {
						continue; // hasn't updated
					}
					valueOld = KvGetFloat(backpackTFPricelist, "value_old");
					valueOldHigh = KvGetFloat(backpackTFPricelist, "value_high_old");
					value = KvGetFloat(backpackTFPricelist, "value");
					valueHigh = KvGetFloat(backpackTFPricelist, "value_high");
					
					KvGetString(backpackTFPricelist, "currency", currency, sizeof(currency));
					KvGetString(backpackTFPricelist, "currency_old", currencyOld, sizeof(currencyOld));
					
					if(strlen(currency) == 0 || strlen(currencyOld) == 0) {
						continue;
					}
					
					FormatPriceRange(valueOld, valueOldHigh, currency, oldPrice, sizeof(oldPrice), StrEqual(qualityIndex, QUALITY_UNUSUAL));
					FormatPriceRange(value, valueHigh, currency, newPrice, sizeof(newPrice), StrEqual(qualityIndex, QUALITY_UNUSUAL));
					
					// Get an average so we can determine if it went up or down
					if(valueOldHigh != 0.0) {
						valueOld = (valueOld + valueOldHigh) / 2.0;
					}
					
					if(valueHigh != 0.0) {
						value = (value + valueHigh) / 2.0;
					}
					
					// Get prices in terms of refined now so we can determine if it went up or down
					if(StrEqual(currencyOld, "earbuds")) {
						valueOld = valueOld * budsToKeys * keysToRef;
					} else if(StrEqual(currencyOld, "keys")) {
						valueOld = valueOld * keysToRef;
					}
					
					if(StrEqual(currency, "earbuds")) {
						value = value * budsToKeys * keysToRef;
					} else if(StrEqual(currency, "keys")) {
						value = value * keysToRef;
					}
					
					difference = value - valueOld;
					if(difference < 0.0) {
						isNegative = true;
						difference = difference * -1.0;
					} else {
						isNegative = false;
					}
					
					// Format a quality name
					if(StrEqual(qualityIndex, QUALITY_UNIQUE)) {
						Format(quality, sizeof(quality), ""); // if quality is unique, don't display a quality
					} else if(StrEqual(qualityIndex, QUALITY_UNUSUAL) && (StringToInt(defindex) != ITEM_HAUNTED_SCRAP && StringToInt(defindex) != ITEM_HEADTAKER)) {
						decl String:effect[16];
						KvGetSectionName(backpackTFPricelist, effect, sizeof(effect));
						if(!GetTrieString(unusualNameTrie, effect, quality, sizeof(quality))) {
							LogError("Unknown unusual effect: %s in OnBackpackTFComplete. Please report this!", effect);
							decl String:kvPath[PLATFORM_MAX_PATH];
							BuildPath(Path_SM, kvPath, sizeof(kvPath), "data/backpack-tf.%d.txt", GetTime());
							if(!FileExists(kvPath)) {
								KeyValuesToFile(backpackTFPricelist, kvPath);
							}
							continue;
						}
					} else {
						if(!GetTrieString(qualityNameTrie, qualityIndex, quality, sizeof(quality))) {
							LogError("Unknown quality index: %s. Please report this!", qualityIndex);
							continue;
						}
					}
					
					Format(message, sizeof(message), "%s%s%s: %s #From %s #To %s", quality, StrEqual(quality, "") ? "" : " ", name, isNegative ? "#Down" : "#Up", oldPrice, newPrice);
					PushArrayString(array, message);
					
				} while(KvGotoNextKey(backpackTFPricelist)); // end: instances
				KvGoBack(backpackTFPricelist);
				
			} while(KvGotoNextKey(backpackTFPricelist)); // end: qualities
			KvGoBack(backpackTFPricelist);
			
		} while(KvGotoNextKey(backpackTFPricelist)); // end: items
	}
	
	SetHudTextParams(GetConVarFloat(cvarHudXPos), GetConVarFloat(cvarHudYPos), GetConVarFloat(cvarHudHoldTime), GetConVarInt(cvarHudRed), GetConVarInt(cvarHudGreen), GetConVarInt(cvarHudBlue), 255);
	for(new i = 1; i <= MaxClients; i++) {
		if(!IsClientInGame(i)) {
			continue;
		}
		ShowSyncHudText(i, hudText, "%t", "Price list updated");
		EmitSoundToClient(i, NOTIFICATION_SOUND);
	}
	CreateTimer(GetConVarFloat(cvarHudHoldTime), Timer_DisplayHudText, array, TIMER_REPEAT);
}

Float:GetConversion(defindex) {
	decl String:buffer[32];
	PrepPriceKv();
	IntToString(defindex, buffer, sizeof(buffer));
	KvJumpToKey(backpackTFPricelist, buffer);
	KvJumpToKey(backpackTFPricelist, "6");
	KvJumpToKey(backpackTFPricelist, "0");
	new Float:value = KvGetFloat(backpackTFPricelist, "value");
	new Float:valueHigh = KvGetFloat(backpackTFPricelist, "value_high");
	if(valueHigh == 0.0) {
		return value;
	}
	return (value + valueHigh) / 2.0;
}

FormatPrice(Float:price, const String:currency[], String:output[], maxlen, bool:includeCurrency = true, bool:forceBuds = false) {
	new String:outputCurrency[32];
	if(StrEqual(currency, "metal")) {
		Format(outputCurrency, sizeof(outputCurrency), "refined");
	} else if(StrEqual(currency, "keys")) {
		Format(outputCurrency, sizeof(outputCurrency), "key");
	} else if(StrEqual(currency, "earbuds")) {
		Format(outputCurrency, sizeof(outputCurrency), "bud");
	} else if(StrEqual(currency, "usd")) {
		if(forceBuds) {
			Format(outputCurrency, sizeof(outputCurrency), "earbuds"); // This allows us to force unusual price ranges to display buds only
		}
		ConvertUSD(price, outputCurrency, sizeof(outputCurrency));
	} else {
		ThrowError("Unknown currency: %s", currency);
	}
	
	if(FloatIsInt(price)) {
		Format(output, maxlen, "%d", RoundToFloor(price));
	} else {
		Format(output, maxlen, "%.2f", price);
	}
	
	if(!includeCurrency) {
		return;
	}
	
	if(StrEqual(output, "1") || StrEqual(currency, "metal")) {
		Format(output, maxlen, "%s %s", output, outputCurrency);
	} else {
		Format(output, maxlen, "%s %ss", output, outputCurrency);
	}
}

FormatPriceRange(Float:low, Float:high, const String:currency[], String:output[], maxlen, bool:forceBuds = false) {
	if(high == 0.0) {
		FormatPrice(low, currency, output, maxlen, true, forceBuds);
		return;
	}
	decl String:buffer[32];
	FormatPrice(low, currency, output, maxlen, false, forceBuds);
	FormatPrice(high, currency, buffer, sizeof(buffer), true, forceBuds);
	Format(output, maxlen, "%s-%s", output, buffer);
}

ConvertUSD(&Float:price, String:outputCurrency[], maxlen) {
	new Float:budPrice = refToUsd * keysToRef * budsToKeys;
	if(price < budPrice && !StrEqual(outputCurrency, "earbuds")) {
		new Float:keyPrice = refToUsd * keysToRef;
		price = price / keyPrice;
		Format(outputCurrency, maxlen, "key");
	} else {
		price = price / budPrice;
		Format(outputCurrency, maxlen, "bud");
	}
}

bool:FloatIsInt(Float:input) {
	return float(RoundToFloor(input)) == input;
}

public Action:Timer_DisplayHudText(Handle:timer, any:array) {
	if(GetArraySize(array) == 0) {
		CloseHandle(array);
		return Plugin_Stop;
	}
	decl String:text[128], String:display[128];
	GetArrayString(array, 0, text, sizeof(text));
	SetHudTextParams(GetConVarFloat(cvarHudXPos), GetConVarFloat(cvarHudYPos), GetConVarFloat(cvarHudHoldTime), GetConVarInt(cvarHudRed), GetConVarInt(cvarHudGreen), GetConVarInt(cvarHudBlue), 255);
	for(new i = 1; i <= MaxClients; i++) {
		if(!IsClientInGame(i)) {
			continue;
		}
		PerformTranslationTokenReplacement(i, text, display, sizeof(display));
		ShowSyncHudText(i, hudText, display);
	}
	RemoveFromArray(array, 0);
	return Plugin_Continue;
}

PerformTranslationTokenReplacement(client, const String:message[], String:output[], maxlen) {
	SetGlobalTransTarget(client);
	strcopy(output, maxlen, message);
	decl String:buffer[64];
	
	Format(buffer, maxlen, "%t", "Type !pc for a price check");
	ReplaceString(output, maxlen, "#Type_command", buffer);
	
	Format(buffer, maxlen, "%t", "Up");
	ReplaceString(output, maxlen, "#Up", buffer);
	
	Format(buffer, maxlen, "%t", "Down");
	ReplaceString(output, maxlen, "#Down", buffer);
	
	Format(buffer, maxlen, "%t", "From");
	ReplaceString(output, maxlen, "#From", buffer);
	
	Format(buffer, maxlen, "%t", "To");
	ReplaceString(output, maxlen, "#To", buffer);
}

PrepPriceKv() {
	KvRewind(backpackTFPricelist);
	KvJumpToKey(backpackTFPricelist, "prices");
}

public Action:Command_PriceCheck(client, args) {
	if(backpackTFPricelist == INVALID_HANDLE) {
		decl String:key[32];
		GetConVarString(cvarAPIKey, key, sizeof(key));
		if(strlen(key) == 0) {
			ReplyToCommand(client, "\x04[SM] \x01The server administrator has not filled in their API key yet. Please contact the server administrator.");
		} else {
			ReplyToCommand(client, "\x04[SM] \x01%t.", "The price list has not loaded yet");
		}
		return Plugin_Handled;
	}
	if(args == 0) {
		new Handle:menu = CreateMenu(Handler_ItemSelection);
		SetMenuTitle(menu, "Price Check");
		PrepPriceKv();
		KvGotoFirstSubKey(backpackTFPricelist);
		decl String:name[128];
		do {
			if(!KvJumpToKey(backpackTFPricelist, "item_info")) {
				continue;
			}
			KvGetString(backpackTFPricelist, "item_name", name, sizeof(name));
			if(KvGetNum(backpackTFPricelist, "proper_name") == 1) {
				Format(name, sizeof(name), "The %s", name);
			}
			AddMenuItem(menu, name, name);
			KvGoBack(backpackTFPricelist);
		} while(KvGotoNextKey(backpackTFPricelist));
		DisplayMenu(menu, client, GetConVarInt(cvarMenuHoldTime));
		return Plugin_Handled;
	}
	new resultDefindex = -1;
	decl String:defindex[8], String:name[128], String:itemName[128];
	GetCmdArgString(name, sizeof(name));
	new bool:exact = StripQuotes(name);
	PrepPriceKv();
	KvGotoFirstSubKey(backpackTFPricelist);
	new Handle:matches;
	if(!exact) {
		matches = CreateArray(128);
	}
	do {
		KvGetSectionName(backpackTFPricelist, defindex, sizeof(defindex));
		if(!KvJumpToKey(backpackTFPricelist, "item_info")) {
			continue;
		}
		KvGetString(backpackTFPricelist, "item_name", itemName, sizeof(itemName));
		if(KvGetNum(backpackTFPricelist, "proper_name") == 1) {
			Format(itemName, sizeof(itemName), "The %s", itemName);
		}
		KvGoBack(backpackTFPricelist);
		if(exact) {
			if(StrEqual(itemName, name, false)) {
				resultDefindex = StringToInt(defindex);
				break;
			}
		} else {
			if(StrContains(itemName, name, false) != -1) {
				resultDefindex = StringToInt(defindex); // In case this is the only match, we store the resulting defindex here so that we don't need to search to find it again
				PushArrayString(matches, itemName);
			}
		}
	} while(KvGotoNextKey(backpackTFPricelist));
	if(!exact && GetArraySize(matches) > 1) {
		new Handle:menu = CreateMenu(Handler_ItemSelection);
		SetMenuTitle(menu, "Search Results");
		new size = GetArraySize(matches);
		for(new i = 0; i < size; i++) {
			GetArrayString(matches, i, itemName, sizeof(itemName));
			AddMenuItem(menu, itemName, itemName);
		}
		DisplayMenu(menu, client, GetConVarInt(cvarMenuHoldTime));
		CloseHandle(matches);
		return Plugin_Handled;
	}
	if(!exact) {
		CloseHandle(matches);
	}
	if(resultDefindex == -1) {
		ReplyToCommand(client, "\x04[SM] \x01No matching item was found.");
		return Plugin_Handled;
	}
	// At this point, we know that we've found our item. Its defindex is stored in resultDefindex as a cell
	// defindex was used to store the defindex of every item as we searched it, so it's not reliable
	if(resultDefindex == ITEM_REFINED) {
		SetGlobalTransTarget(client);
		new Handle:menu = CreateMenu(Handler_PriceListMenu);
		SetMenuTitle(menu, "%t\n%t\n%t\n ", "Price check", itemName, "Prices are estimates only", "Prices courtesy of backpack.tf");
		decl String:buffer[32];
		Format(buffer, sizeof(buffer), "Unique: $%.2f USD", refToUsd);
		AddMenuItem(menu, "", buffer);
		DisplayMenu(menu, client, GetConVarInt(cvarMenuHoldTime));
		return Plugin_Handled;
	}
	new bool:isCrate = (resultDefindex == ITEM_CRATE || resultDefindex == ITEM_SALVAGED_CRATE);
	new bool:onlyOneUnusual = (resultDefindex == ITEM_HEADTAKER || resultDefindex == ITEM_HAUNTED_SCRAP);
	PrepPriceKv();
	IntToString(resultDefindex, defindex, sizeof(defindex));
	KvJumpToKey(backpackTFPricelist, defindex);
	KvJumpToKey(backpackTFPricelist, "item_info");
	KvGetString(backpackTFPricelist, "item_name", itemName, sizeof(itemName));
	if(KvGetNum(backpackTFPricelist, "proper_name") == 1) {
		Format(itemName, sizeof(itemName), "The %s", itemName);
	}
	KvGotoNextKey(backpackTFPricelist);
	
	SetGlobalTransTarget(client);
	new Handle:menu = CreateMenu(Handler_PriceListMenu);
	SetMenuTitle(menu, "%t\n%t\n%t\n ", "Price check", itemName, "Prices are estimates only", "Prices courtesy of backpack.tf");
	new bool:unusualDisplayed = false;
	new Float:value, Float:valueHigh;
	decl String:currency[32], String:qualityIndex[16], String:quality[16], String:series[8], String:price[32], String:buffer[64];
	do {
		KvGetSectionName(backpackTFPricelist, qualityIndex, sizeof(qualityIndex));
		if(StrEqual(qualityIndex, "item_info") || StrEqual(qualityIndex, "alt_defindex")) {
			continue;
		}
		KvGotoFirstSubKey(backpackTFPricelist);
		do {
			if(StrEqual(qualityIndex, QUALITY_UNUSUAL) && !onlyOneUnusual) {
				if(!unusualDisplayed) {
					AddMenuItem(menu, defindex, "Unusual: View Effects");
					unusualDisplayed = true;
				}
			} else {
				value = KvGetFloat(backpackTFPricelist, "value");
				valueHigh = KvGetFloat(backpackTFPricelist, "value_high");
				KvGetString(backpackTFPricelist, "currency", currency, sizeof(currency));
				FormatPriceRange(value, valueHigh, currency, price, sizeof(price));
				
				if(!GetTrieString(qualityNameTrie, qualityIndex, quality, sizeof(quality))) {
					LogError("Unknown quality index: %s. Please report this!", qualityIndex);
					continue;
				}
				if(isCrate) {
					KvGetSectionName(backpackTFPricelist, series, sizeof(series));
					if(StrEqual(series, "0")) {
						continue;
					}
					if(StrEqual(qualityIndex, QUALITY_UNIQUE)) {
						Format(buffer, sizeof(buffer), "Series %s: %s", series, price);
					} else {
						Format(buffer, sizeof(buffer), "%s: Series %s: %s", quality, series, price);
					}
				} else {
					Format(buffer, sizeof(buffer), "%s: %s", quality, price);
				}
				AddMenuItem(menu, "", buffer, ITEMDRAW_DISABLED);
			}
		} while(KvGotoNextKey(backpackTFPricelist));
		KvGoBack(backpackTFPricelist);
	} while(KvGotoNextKey(backpackTFPricelist));
	DisplayMenu(menu, client, GetConVarInt(cvarMenuHoldTime));
	return Plugin_Handled;
}

public Handler_ItemSelection(Handle:menu, MenuAction:action, client, param) {
	if(action == MenuAction_End) {
		CloseHandle(menu);
	}
	if(action != MenuAction_Select) {
		return;
	}
	decl String:selection[128];
	GetMenuItem(menu, param, selection, sizeof(selection));
	FakeClientCommand(client, "sm_pricecheck \"%s\"", selection);
}

public Handler_PriceListMenu(Handle:menu, MenuAction:action, client, param) {
	if(action == MenuAction_End) {
		CloseHandle(menu);
	}
	if(action != MenuAction_Select) {
		return;
	}
	decl String:defindex[32];
	GetMenuItem(menu, param, defindex, sizeof(defindex));
	
	decl String:name[64];
	PrepPriceKv();
	KvJumpToKey(backpackTFPricelist, defindex);
	KvJumpToKey(backpackTFPricelist, "item_info");
	KvGetString(backpackTFPricelist, "item_name", name, sizeof(name));
	if(KvGetNum(backpackTFPricelist, "proper_name") == 1) {
		Format(name, sizeof(name), "The Unusual %s", name);
	} else {
		Format(name, sizeof(name), "Unusual %s", name);
	}
	KvGoBack(backpackTFPricelist);
	
	if(!KvJumpToKey(backpackTFPricelist, QUALITY_UNUSUAL)) {
		return;
	}
	
	KvGotoFirstSubKey(backpackTFPricelist);
	
	SetGlobalTransTarget(client);
	new Handle:menu2 = CreateMenu(Handler_PriceListMenu);
	SetMenuTitle(menu2, "%t\n%t\n%t\n ", "Price check", name, "Prices are estimates only", "Prices courtesy of backpack.tf");
	decl String:effect[8], String:effectName[64], String:message[128], String:price[64], String:currency[32];
	new Float:value, Float:valueHigh;
	do {
		KvGetSectionName(backpackTFPricelist, effect, sizeof(effect));
		if(!GetTrieString(unusualNameTrie, effect, effectName, sizeof(effectName))) {
			LogError("Unknown unusual effect: %s in Handler_PriceListMenu. Please report this!", effect);
			decl String:path[PLATFORM_MAX_PATH];
			BuildPath(Path_SM, path, sizeof(path), "data/backpack-tf.%d.txt", GetTime());
			if(!FileExists(path)) {
				KeyValuesToFile(backpackTFPricelist, path);
			}
			continue;
		}
		value = KvGetFloat(backpackTFPricelist, "value");
		valueHigh = KvGetFloat(backpackTFPricelist, "value_high");
		KvGetString(backpackTFPricelist, "currency", currency, sizeof(currency));
		if(StrEqual(currency, "")) {
			continue;
		}
		FormatPriceRange(value, valueHigh, currency, price, sizeof(price), true);
		
		Format(message, sizeof(message), "%s: %s", effectName, price);
		AddMenuItem(menu2, "", message, ITEMDRAW_DISABLED);
	} while(KvGotoNextKey(backpackTFPricelist));
	DisplayMenu(menu2, client, GetConVarInt(cvarMenuHoldTime));
}

public Action:Command_Backpack(client, args) {
	if(!GetConVarBool(cvarBPCommand)) {
		return Plugin_Continue;
	}
	new target;
	if(args == 0) {
		target = GetClientAimTarget(client);
		if(target <= 0) {
			DisplayClientMenu(client);
			return Plugin_Handled;
		}
	} else {
		decl String:arg1[MAX_NAME_LENGTH];
		GetCmdArg(1, arg1, sizeof(arg1));
		target = FindTargetEx(client, arg1, true, false, false);
		if(target == -1) {
			DisplayClientMenu(client);
			return Plugin_Handled;
		}
	}
	decl String:steamID[64];
	//Steam_GetCSteamIDForClient(target, steamID, sizeof(steamID)); // we could use the regular Steam ID, but we already have SteamTools, so we can just bypass backpack.tf's redirect directly
	GetClientAuthId(target, AuthId_SteamID64, steamID, sizeof(steamID), false);

	decl String:url[256];
	Format(url, sizeof(url), "http://backpack.tf/profiles/%s?t=%i", steamID, GetTime());
	AdvMOTD_ShowMOTDPanel(client, "backpack.tf", url, MOTDPANEL_TYPE_URL, true, true, true, OnMOTDFailure);	
	return Plugin_Handled;
}

public OnMOTDFailure(client, MOTDFailureReason:reason) {
	switch(reason) {
		case MOTDFailure_Disabled: PrintToChat(client, "\x04[SM] \x01You cannot view backpacks with HTML MOTDs disabled.");
		case MOTDFailure_Matchmaking: PrintToChat(client, "\x04[SM] \x01You cannot view backpacks after joining via Quickplay.");
		case MOTDFailure_QueryFailed: PrintToChat(client, "\x04[SM] \x01Unable to open backpack.");
	}
}

DisplayClientMenu(client) {
	new Handle:menu = CreateMenu(Handler_ClientMenu);
	SetMenuTitle(menu, "Select Player");
	decl String:name[MAX_NAME_LENGTH], String:index[8];
	for(new i = 1; i <= MaxClients; i++) {
		if(!IsClientInGame(i) || IsFakeClient(i)) {
			continue;
		}
		GetClientName(i, name, sizeof(name));
		IntToString(GetClientUserId(i), index, sizeof(index));
		AddMenuItem(menu, index, name);
	}
	DisplayMenu(menu, client, GetConVarInt(cvarMenuHoldTime));
}

public Handler_ClientMenu(Handle:menu, MenuAction:action, client, param) {
	if(action == MenuAction_End) {
		CloseHandle(menu);
	}
	if(action != MenuAction_Select) {
		return;
	}
	decl String:selection[32];
	GetMenuItem(menu, param, selection, sizeof(selection));
	FakeClientCommand(client, "sm_backpack #%s", selection);
}

public Action:Command_UpdatePrices(client, args) {
	new age = GetCachedPricesAge();
	if(age != -1 && age < 900) { // 15 minutes
		ReplyToCommand(client, "\x04[SM] \x01The price list cannot be updated more frequently than every 15 minutes. It is currently %d minutes old.", age / 60);
		return Plugin_Handled;
	}
	ReplyToCommand(client, "\x04[SM] \x01Updating backpack.tf prices...");
	Timer_Update(INVALID_HANDLE);
	return Plugin_Handled;
}

FindTargetEx(client, const String:target[], bool:nobots = false, bool:immunity = true, bool:replyToError = true) {
	decl String:target_name[MAX_TARGET_LENGTH];
	decl target_list[1], target_count, bool:tn_is_ml;
	
	new flags = COMMAND_FILTER_NO_MULTI;
	if(nobots) {
		flags |= COMMAND_FILTER_NO_BOTS;
	}
	if(!immunity) {
		flags |= COMMAND_FILTER_NO_IMMUNITY;
	}
	
	if((target_count = ProcessTargetString(
			target,
			client, 
			target_list, 
			1, 
			flags,
			target_name,
			sizeof(target_name),
			tn_is_ml)) > 0)
	{
		return target_list[0];
	} else {
		if(replyToError) {
			ReplyToTargetError(client, target_count);
		}
		return -1;
	}
}