function TGame(url)
{
	var P_STEP_UNIT_SCORE = 0, P_LEVELS_STEPS = 1, P_NEXT_SCORE_GHOST = 2, P_SPAWN_DAS_LOCK_CLEAR = 3, P_GRAVITY = 4, P_ROTATION = 5, P_GAME = 6;

	var parameters = url.replace(/^.*tessellate-.,/, '').replace(/\/.*$/, '');
	var mainSplit = defaultSplit(parameters, "*", "*a");
	this.disabledOptions = translateInt("disabledOptions", mainSplit[1], 0);

	//translateIntList("test", 1, 'k', 4, 10);

	var rawBits = expandEmptyFields(mainSplit[0]).split(",");
	var bits = ["","","","","","",""];
	for (var i = 0; i < rawBits.length; i++)
		bits[i] = rawBits[i];

	this.stepUnit = 'p';
	this.scoreType = 's';
	this.stepUnitMultiplier = 1;
	if (bits[P_STEP_UNIT_SCORE].length > 0)
	{
		this.stepUnit = bits[P_STEP_UNIT_SCORE].charAt(0);
		if (bits[P_STEP_UNIT_SCORE].length > 1)
		{
			this.scoreType = bits[P_STEP_UNIT_SCORE].charAt(1);
			if (bits[P_STEP_UNIT_SCORE].length > 2)
			{
				this.stepUnitMultiplier = translateInt("stepUnitMultiplier", ""+bits[P_STEP_UNIT_SCORE].charAt(2), 1);
			}
		}
	}

	var levelsSteps = bits[P_LEVELS_STEPS].split("_");
	this.levels = translateIntList("levels", 0, levelsSteps[0], 0);
	this.steps = levelsSteps.length >= 2 ? translateIntList("steps", 0, levelsSteps[1], 1) : this.levels;
	if (this.levels[this.levels.length-1] == 0)
	{
		this.numLevels = this.levels.length-1;
		this.numSteps = this.steps.length-1;
		this.creditsDuration = this.steps[this.steps.length-1];
	}
	else
	{
		this.numLevels = this.levels.length;
		this.numSteps = this.steps.length;
		this.creditsDuration = 0;
	}

	var ltotal = 0;
	for (var i = 0; i < this.numLevels; i++)
	{
		ltotal += this.levels[i];
		if (this.levels[i] < 1)
			throw new RuntimeException("Cannot have empty level");
	}
	var stotal = 0;
	for (var i = 0; i < this.numSteps; i++)
		stotal += this.steps[i];
	if (stotal != ltotal)
		throw "Levels and steps do not finish game at the same point (" + ltotal + " vs " + stotal + ")";

	this.totalMicrosteps = stotal;
	var levelData = defaultSplit(bits[P_NEXT_SCORE_GHOST], "_", "b_F_n_a_a_k_u_a_a_a_a");
	this.lnext = translateIntList("next", this.levels.length, levelData[0], 1, 8);
	this.lscoreFlags = translateIntList("scoreFlags", this.levels.length, levelData[1], 0);
	this.lflags = translateIntList("flags", this.levels.length, levelData[2], 0);
	this.lgarbageDelay = translateIntList("garbageDelay", this.levels.length, levelData[3], 0);
	this.lpieceGimmick = translateIntList("pieceGimmick", this.levels.length, levelData[4], 0);
	this.lcols = translateIntList("cols", this.levels.length, levelData[5], 4, 10);
	this.lrows = translateIntList("rows", this.levels.length, levelData[6], 4, 20);
	this.lgarbageType = translateIntList("garbageType", this.levels.length, levelData[7], 0);
	this.lgarbageCounterType = translateIntList("garbageCounterType", this.levels.length, levelData[8], 0);
	this.ltimeLimit = translateIntList("timeLimit", this.levels.length, levelData[9], 0);
	this.lvanishTimeout = translateIntList("vanishTimeout", this.levels.length, levelData[10], 0);

	var timings = defaultSplit(bits[P_SPAWN_DAS_LOCK_CLEAR], "_", "z_p_E_E_._a");
	this.sspawnDelay = translateIntList("spawnDelay", this.steps.length, timings[0], 0);
	this.sdas = translateIntList("das", this.steps.length, timings[1], 0);
	this.slockDelay = translateIntList("lockDelay", this.steps.length, timings[2], 1);
	this.sclearDelay = translateIntList("clearDelay", this.steps.length, timings[3], 0);
	this.sclearSpawnDelay = timings[4] == "." ? this.sspawnDelay : translateIntList("clearSpawnDelay", this.steps.length, timings[4], 0);

	var gravityStuff = defaultSplit(bits[P_GRAVITY], "_", "b_bi_a_a"); // _a_a is automatically changed to be the same as _bi
	this.gravityDenominator = translateInt("gravityDenominator", gravityStuff[1], 1);
	this.sgravity = translateIntList("gravity", this.steps.length, gravityStuff[0], 0);
	this.smoveSpeed = translateIntList("moveSpeed", this.steps.length, gravityStuff[2], 0);
	for (var i = 0; i < this.smoveSpeed.length; i++)
		if (this.smoveSpeed[i] == 0)
			this.smoveSpeed[i] = this.gravityDenominator;
	this.ssoftSpeed = translateIntList("softSpeed", this.steps.length, gravityStuff[3], 0);
	for (var i = 0; i < this.ssoftSpeed.length; i++)
		if (this.ssoftSpeed[i] == 0)
			this.ssoftSpeed[i] = this.gravityDenominator;

	var rotationStuff = defaultSplit(bits[P_ROTATION], "_", "jac_b");
	this.rotationCode = rotationStuff[0].charAt(0);
	this.lockResetType = rotationStuff[0].length > 1 ? translateInt("lockReset", ""+rotationStuff[0].charAt(1), 0) : 0;
	this.diagonalMovement = rotationStuff[0].length > 2 ? translateInt("diagonalMovement", ""+rotationStuff[0].charAt(2), 0) : 0;
	this.maxFloorKicks = translateInt("floorKicks", ""+rotationStuff[1].charAt(0), 0);
	this.randomizerCode = rotationStuff[1].length > 1 ? rotationStuff[1].charAt(1) : (this.rotationCode == 'h' ? 'h' : 'g');
	this.enterAbove = rotationStuff[1].length > 2 ? (rotationStuff[1].charAt(2) == 'b') : (this.rotationCode == 'h');

	// readyDuration+goDuration_readyGoDASType_levelClearDuration_gameClearDuration_dasBaldSpots_initialSpawnDelay
	var gameStuff = defaultSplit(bits[P_GAME], "_", "Y_c_$de_bh_a");
	var readyGoDurations = translateIntList("readyGoDurations", 2, gameStuff[0], 0);
	this.readyDuration = readyGoDurations[0];
	this.goDuration = readyGoDurations[1];
	this.readyGoDASType = translateInt("readyGoDASType", gameStuff[1], 0);
	var clearDurations = translateIntList("clearDurations", 2, gameStuff[2], 0);
	this.levelClearDuration = clearDurations[0];
	this.gameClearDuration = clearDurations[1];
	this.dasBaldSpots = translateInt("dasBaldSpots", gameStuff[3], 0);
	this.initialSpawnDelay = translateInt("initialSpawnDelay", gameStuff[4], 0);


	this.speed = function(value)
	{
		var gcd = gcd2(value, this.gravityDenominator);
		var speed = value / gcd;
		var denom = this.gravityDenominator / gcd;
		var text = speed;
		if (denom != 1)
			text += "/"+denom;
		text += " G";
		return text;
	}

	this.stepLabel = function()
	{
		var result;
		switch (this.stepUnit)
		{
			case 's': result = 'Seconds'; break;
			case 'l': result = 'Lines'; break;
			case 'p': result = 'Pieces'; break;
			case 'v':
			case 'c': result = 'Pieces and Lines'; break;
			case 'w':
			case 'b': result = 'Pieces and Lines and Big Line Clears'; break;
			case 'g': result = 'Garbage Lines'; break;
			default: result = 'Unknown ('+this.stepUnit+')'; break;
		}
		if (this.stepUnitMultiplier > 1)
			result += " x" + stepUnitMultiplier;
		return result;
	}
	this.rotationText = function()
	{
		switch (this.rotationCode)
		{
			case 'j': return 'Japanese';
			case 'k': return 'Japanese with extra kicks';
			case 'h': return 'Hawaiian';
			default: return 'Unknown';
		}
	}
	this.diagonalMovementText = function()
	{
		switch (this.diagonalMovement)
		{
			case 0: return 'Horizontal overrides vertical';
			case 2: return 'Vertical overrides horizontal';
			case 3: return 'Continues in previous direction';
			case 4: return 'Moves in both directions';
			default: return 'Unknown ('+this.diagonalMovement+')';
		}
	}
	this.lockResetTypeText = function()
	{
		switch (this.lockResetType)
		{
			case 0: return 'Step reset';
			case 1: return 'Strong step reset';
			case 2: return 'Entry reset';
			case 3: return 'Move reset';
			case 4: return 'Expire in air';
			default: return 'Unknown ('+this.lockResetType+')';
		}
	}
	this.garbageText = function(i)
	{
		switch (this.lgarbageType[i])
		{
			case 0: return 'none';
			case 1: return 'Copy bottom row';
			case 2: return 'Piece-shaped holes';
			case 3: return 'New hole every row';
			case 4: return 'New hole every 2nd row';
			case 5: return 'New hole every 3rd row';
			case 6: return 'New hole every 4th row';
			case 13: return 'Two new holes every row';
			case 14: return 'Two new holes every 2nd row';
			case 15: return 'Two new holes every 3rd row';
			case 16: return 'Two new holes every 4th row';
			case 23: return 'Zigzag';
			case 24: return 'Jagged left';
			case 25: return 'Jagged right';
			case 26: return 'Big checkers';
			case 28: return 'Outside 1 column';
			case 29: return 'Outside 2 columns';
			case 30: return 'Outside 3 columns';
			case 31: return 'Outside 4 columns';
			default: return 'Unknown ('+this.lgarbageType[i]+')';
		}
	}
	this.pieceGimmickText = function(i)
	{
		switch (this.lpieceGimmick[i])
		{
			case 0: return 'none';
			case 1: return 'invisible';
			case 2: return 'IJLOSTZ';
			case 3: return '[ ]';
			case 4: return '#';
			default: return 'Unknown ('+this.lpieceGimmick[i]+')';
		}
	}
	this.wellSizeText = function(i)
	{
		return this.lcols[i]+'x'+this.lrows[i];
	}
	this.scoringText = function(i)
	{
		if (!this.scoringApplicable())
			return 'N/A';

		var text = '';
		if (this.lscoreFlags[i] & 1) text += 'rows, ';
		if (this.lscoreFlags[i] & 2) text += 'speed, ';
		if (this.lscoreFlags[i] & 4) text += 'combos, ';
		if (this.lscoreFlags[i] & 8) text += 'level, ';
		if (this.lscoreFlags[i] & 16) text += 'twists, ';
		if (text == '')
			text = 'None ('+this.lscoreFlags[i]+')';
		else
			text = text.replace(/, $/, '');
		return text;
	}
	this.scoringApplicable = function(i)
	{
		return this.scoreType == 's' || this.scoreType == 'q' || this.scoreType == 'm' || this.scoreType == 'w' || this.scoreType == 'f';
	}

	this.displayHtml = function()
	{
		var html = '';
		html += '<table class="lgrid">';
		html += '<tr><td>'+this.stepLabel()+':</td><td>'+this.totalMicrosteps+((this.stepUnit=='v'||this.stepUnit=='w')?' with level blocks':'')+'</td></tr>';
		if (this.disabledOptions & 2 == 0)
			html += '<tr><td>Rotation system:</td><td>'+this.rotationText()+'</td></tr>';
		if (this.disabledOptions & 4 == 0)
		html += '<tr><td>Diagonal movement:</td><td>'+this.diagonalMovementText()+'</td></tr>';
		html += '<tr><td>Lock reset:</td><td>'+this.lockResetTypeText()+'</td></tr>';
		if (this.rotationCode == 'h' || this.rotationCode == 'k')
			html += '<tr><td>Allowed floor kicks:</td><td>'+this.maxFloorKicks+'</td></tr>';
		html += '</table>';

		html += '<h3>Level information</h3>';
		html += '<table class="grid">';
		html += '<thead>';
		html += '<tr>';
		if (this.levels.length > 1)
			html += '<th>'+this.stepLabel()+' per level</th>';
		html += '<th>Next pieces</th>';
		html += '<th>Hold</th>';
		html += '<th>Ghost</th>';
		if (!allEqual(this.lgarbageType, 0))
			html += '<th>Garbage</th>';
		if (!allEqual(this.lpieceGimmick, 0))
			html += '<th>Piece gimmick</th>';
		if (!allEqual(this.lvanishTimeout, 0))
			html += '<th>Vanish timeout</th>';
		html += '<th>Well size</th>';
		if (!allFlagsEqual(this.lflags, 2, 0))
			html += '<th>Clear after</th>';
		if (!allEqual(this.ltimeLimit, 0))
		html += '<th>Time limit</th>';
		html += '<th>Double-lock protection</th>';
		if (this.scoringApplicable())
			html += '<th>Scoring</th>';
		html += '</tr>';
		html += '</thead>';
		html += '<tbody>';
		for (var i = 0; i < this.levels.length; i++)
		{
			var flags = this.lflags[i];
			html += '<tr>';
			if (this.levels.length > 1)
			{
				if (this.levels[i] == 0)
					html += '<td>Credit roll</td>';
				else
					html += '<td>'+this.levels[i]+'</td>';
			}
			html += '<td>'+this.lnext[i]+'</td>';
			html += '<td>'+yes(flags&8)+'</td>';
			html += '<td>'+yes(flags&1)+'</td>';
			if (!allEqual(this.lgarbageType, 0))
				html += '<td>'+this.garbageText(i)+'</td>';
			if (!allEqual(this.lpieceGimmick, 0))
				html += '<td>'+this.pieceGimmickText(i)+'</td>';
			if (!allEqual(this.lvanishTimeout, 0))
				html += '<td>'+this.lvanishTimeout[i]+' frames</td>';
			html += '<td>'+this.wellSizeText(i)+'</td>';
			if (!allFlagsEqual(this.lflags, 2, 0))
				html += '<td>'+yes(flags&2)+'</td>';
			if (!allEqual(this.ltimeLimit, 0))
				html += '<td>'+(this.ltimeLimit[i] > 0 ? secondsStr(this.ltimeLimit[i]) : '')+'</td>';
			html += '<td>'+yes(flags&4)+'</td>';
			if (this.scoringApplicable())
				html += '<td>'+this.scoringText(i)+'</td>';
			html += '</tr>'
		}
		html += '</tbody>';
		html += '</table>';
		if (this.disabledOptions & 1)
		{
			html += '<h3>Timings</h3>';
			html += '<table class="grid">';
			html += '<thead>';
			html += '<tr><th>'+this.stepLabel()+' per step</th><th>Spawn delay</th><th>Clear spawn delay</th><th>DAS</th><th>Lock delay</th><th>Clear time</th><th>Gravity</th><th>Move speed</th><th>Soft drop speed</th></tr>';
			html += '</thead>';
			html += '<tbody>';
			for (var i = 0; i < this.steps.length; i++)
			{
				html += '<tr>';
				if (this.numSteps != this.steps.length && i == this.steps.length-1)
					html += '<td>Credit roll ('+this.steps[i]+' frames)</td>';
				else
					html += '<td>'+this.steps[i]+'</td>';
				html += '<td>'+this.sspawnDelay[i]+'</td>';
				html += '<td>'+this.sclearSpawnDelay[i]+'</td>';
				html += '<td>'+this.sdas[i]+'</td>';
				html += '<td>'+this.slockDelay[i]+'</td>';
				html += '<td>'+this.sclearDelay[i]+'</td>';
				html += '<td>'+this.speed(this.sgravity[i])+'</td>';
				html += '<td>'+this.speed(this.smoveSpeed[i])+'</td>';
				html += '<td>'+this.speed(this.ssoftSpeed[i])+'</td>';
				html += '</tr>';
			}
			html += '</tbody>';
			html += '</table>';
		}
		return html;
	}
}

function defaultSplit(str, delim, defaultStr)
{
	var result = defaultStr.split(delim);
	var override = str.length == 0 ? new Array() : str.split(delim);
	for (var i = 0; i < override.length; i++)
		if (override[i].length > 0)
			result[i] = override[i];
	return result;
}
function translateIntList(name, fill, str, min)
{
	var MAX_INT = 0x7fffffff;
	return translateIntList(name, fill, str, min, MAX_INT);
}
function translateIntList(name, fill, str, min, max)
{
	var intList = new Array();
	var k = 0;
	var width = 1;
	for (var i = 0; i < str.length; i++)
	{
		var c = str.charAt(i);
		if (c == '$') {
			width++;
		} else if (c == '!' && width > 1) {
			width--;
		} else {
			var n = 0;
			for (var j = 0; j < width; j++, i++)
			{
				n = n*52 + charToInt(str.charAt(i));
			}
			if (n < min || n > max)
				throw (name+": outside of valid range ("+n+" < "+min+" || "+n+" > "+max+")");
			var m = 0;
			for (; i < str.length && isDigit(str.charAt(i)); i++)
			{
				m = m*10+(str.charCodeAt(i)-'0'.charCodeAt(0));
			}
			if (m == 0) m = 1;
			for (var j = 0; j < m; j++)
			{
				intList[k++] = n;
			}

			i--;
		}
	}

	var n = intList[intList.length-1];
	while (intList.length < fill)
		intList[k++] = n;

	return intList;
}

function charToInt(c)
{
	var d = c.charCodeAt(0);
	if (d >= 'a'.charCodeAt(0) && d <= 'z'.charCodeAt(0))
		return d-'a'.charCodeAt(0);
	else if (d >= 'A'.charCodeAt(0) && d <= 'Z'.charCodeAt(0))
		return d-'A'.charCodeAt(0)+26;
	else
		throw "char code '"+c+"' ("+d+") out of range";
}
function translateInt(name, str, min)
{
	var width = str.length;
	var n = 0;
	for (var i = 0; i < width; i++)
	{
		n = n*52 + charToInt(str.charAt(i));
	}
	if (n < min)
		throw name+" < "+min;
	return n;
}


function isDigit(c)
{
	return c.match(/^[0-9]$/);
}

function yes(n)
{
	return n ? 'yes' : 'no';
}

function secondsStr(sec)
{
	if (sec > 60)
		return parseInt(sec/60)+":"+(sec%60)+':00';
	else if (sec == 60)
		return "1:00:00";
	else
		return "00:"+(sec%60)+':00';
}

function gcd2(a, b)
{
	if (a == 0) return b;
	if (b == 0) return a;
	var remainder = a % b;
	while (remainder != 0)
	{
		a = b;
		b = remainder;
		remainder = a % b;
	}
	return b;
}

function allFlagsEqual(a, flag, value)
{
	for (var i = 0; i < a.length; i++)
		if ((a[i] & flag) != value)
			return false;
	return true;
}
function allEqual(a, value)
{
	for (var i = 0; i < a.length; i++)
		if (a[i] != value)
			return false;
	return true;
}

function expandEmptyFields(str)
{
	var out = '';
	for (var i = 0; i < str.length; i++)
	{
		var c = str.charAt(i);
		out += c;
		if (c == ',' || c == '_')
		{
			var n = 0;
			i++;
			for (; i < str.length && isDigit(str.charAt(i)); i++)
			{
				n = n * 10 + (str.charCodeAt(i)-'0'.charCodeAt(0));
			}
			i--;
			for (var j = 1; j < n; j++)
				out += c;
		}
	}
	return out;
}


function eliminateEmptyFields(str)
{
	var out = '';
	var prev = '';
	var count = 1;
	for (var i = 0; i < str.length; i++)
	{
		var c = str.charAt(i);
		if ((c == ',' || c == '_') && c == prev)
		{
			count++;
		}
		else
		{
			if (count > 1)
			{
				out += count;
				count = 1;
			}
			out += c;
		}
		prev = c;
	}
	if (count > 1)
	{
		out += count;
		count = 1;
	}
	return out;
}


