/**
* @constructor
*
* @param {number} type     Tag's type
* @param {string} name     Name of the tag
* @param {number} pos      Position of the tag in the text
* @param {number} len      Length of text consumed by the tag
* @param {number=} priority This tag's sorting tiebreaker
*/
function Tag(type, name, pos, len, priority)
{
	this.type = +type;
	this.name = name;
	this.pos  = +pos;
	this.len  = +len;
	this.sortPriority = +priority || 0;

	this.attributes = {};
	this.cascade    = [];

	// Invalidate this tag now if any value is not a number, they could wreck
	// havoc in other parts of the program
	if (isNaN(type + pos + len))
	{
		this.invalidate();
	}
}

/** @const */
Tag.START_TAG = 1;

/** @const */
Tag.END_TAG = 2;

/** @const */
Tag.SELF_CLOSING_TAG = 3;

/**
* @type {!Object} Dictionary of attributes
*/
Tag.prototype.attributes;

/**
* @type {!Array.<!Tag>} List of tags that are invalidated when this tag is invalidated
*/
Tag.prototype.cascade;

/**
* @type {?Tag} End tag that unconditionally ends this start tag
*/
Tag.prototype.endTag;

/**
* @type {boolean} Whether this tag is be invalid
*/
Tag.prototype.invalid = false;

/**
* @type {number} Length of text consumed by this tag
*/
Tag.prototype.len;

/**
* @type {string} Name of this tag
*/
Tag.prototype.name;

/**
* @type {number} Position of this tag in the text
*/
Tag.prototype.pos;

/**
* @type {number} Tiebreaker used when sorting identical tags
*/
Tag.prototype.sortPriority;

/**
* @type {?Tag} Start tag that is unconditionally closed this end tag
*/
Tag.prototype.startTag;

/**
* @type {number} Tag type
*/
Tag.prototype.type;

/**
* Add a set of flags to this tag's
*
* @param {number} flags
*/
Tag.prototype.addFlags = function(flags)
{
	this.flags |= flags;
};

/**
* Set given tag to be invalidated if this tag is invalidated
*
* @param {!Tag} tag
*/
Tag.prototype.cascadeInvalidationTo = function(tag)
{
	this.cascade.push(tag);

	// If this tag is already invalid, cascade it now
	if (this.invalid)
	{
		tag.invalidate();
	}
};

/**
* Invalidate this tag, as well as tags bound to this tag
*/
Tag.prototype.invalidate = function()
{
	// Only invalidate if this tag is valid to prevent infinite loops
	if (!this.invalid)
	{
		this.invalid = true;
		this.cascade.forEach(
			/**
			* @param {!Tag} tag
			*/
			function(tag)
			{
				tag.invalidate();
			}
		);
	}
};

/**
* Pair this tag with given tag
*
* @param {!Tag} tag
*/
Tag.prototype.pairWith = function(tag)
{
	if (this.canBePaired(this, tag))
	{
		this.endTag  = tag;
		tag.startTag = this;

		this.cascadeInvalidationTo(tag);
	}
	else if (this.canBePaired(tag, this))
	{
		this.startTag = tag;
		tag.endTag    = this;
	}
};

/**
* Test whether two tags can be paired
*
* @param  {!Tag} startTag
* @param  {!Tag} endTag
* @return {boolean}
*/
Tag.prototype.canBePaired = function(startTag, endTag)
{
	return startTag.name === endTag.name && startTag.type === Tag.START_TAG && endTag.type === Tag.END_TAG && startTag.pos <= startTag.pos;
};

/**
* Remove a set of flags from this tag's
*
* @param {number} flags
*/
Tag.prototype.removeFlags = function(flags)
{
	this.flags &= ~flags;
};

/**
* Set the bitfield of boolean rules that apply to this tag
*
* @param {number} flags Bitfield of boolean rules that apply to this tag
*/
Tag.prototype.setFlags = function(flags)
{
	this.flags = flags;
};

//==========================================================================
// Getters
//==========================================================================

/**
* Return this tag's attributes
*
* @return {!Object}
*/
Tag.prototype.getAttributes = function()
{
	let attributes = {};
	for (let attrName in this.attributes)
	{
		attributes[attrName] = this.attributes[attrName];
	}

	return attributes;
};

/**
* Return this tag's end tag
*
* @return {?Tag} This tag's end tag
*/
Tag.prototype.getEndTag = function()
{
	return this.endTag;
};

/**
* Return the bitfield of boolean rules that apply to this tag
*
* @return {number}
*/
Tag.prototype.getFlags = function()
{
	return this.flags;
};

/**
* Return the length of text consumed by this tag
*
* @return {number}
*/
Tag.prototype.getLen = function()
{
	return this.len;
};

/**
* Return this tag's name
*
* @return {string}
*/
Tag.prototype.getName = function()
{
	return this.name;
};

/**
* Return this tag's position
*
* @return {number}
*/
Tag.prototype.getPos = function()
{
	return this.pos;
};

/**
* Return this tag's tiebreaker
*
* @return {number}
*/
Tag.prototype.getSortPriority = function()
{
	return this.sortPriority;
};

/**
* Return this tag's start tag
*
* @return {?Tag} This tag's start tag
*/
Tag.prototype.getStartTag = function()
{
	return this.startTag;
};

/**
* Return this tag's type
*
* @return {number}
*/
Tag.prototype.getType = function()
{
	return this.type;
};

//==========================================================================
// Tag's status
//==========================================================================

/**
* Test whether this tag can close given start tag
*
* @param  {!Tag} startTag
* @return {boolean}
*/
Tag.prototype.canClose = function(startTag)
{
	if (this.invalid
	 || !this.canBePaired(startTag, this)
	 || (this.startTag && this.startTag !== startTag)
	 || (startTag.endTag && startTag.endTag !== this))
	{
		return false;
	}

	return true;
};

/**
* Test whether this tag is a br tag
*
* @return {boolean}
*/
Tag.prototype.isBrTag = function()
{
	return (this.name === 'br');
};

/**
* Test whether this tag is an end tag (self-closing tags inclusive)
*
* @return {boolean}
*/
Tag.prototype.isEndTag = function()
{
	return !!(this.type & Tag.END_TAG);
};

/**
* Test whether this tag is an ignore tag
*
* @return {boolean}
*/
Tag.prototype.isIgnoreTag = function()
{
	return (this.name === 'i');
};

/**
* Test whether this tag is invalid
*
* @return {boolean}
*/
Tag.prototype.isInvalid = function()
{
	return this.invalid;
};

/**
* Test whether this tag represents a paragraph break
*
* @return {boolean}
*/
Tag.prototype.isParagraphBreak = function()
{
	return (this.name === 'pb');
};

/**
* Test whether this tag is a self-closing tag
*
* @return {boolean}
*/
Tag.prototype.isSelfClosingTag = function()
{
	return (this.type === Tag.SELF_CLOSING_TAG);
};

/**
* Test whether this tag is a special tag: "br", "i", "pb" or "v"
*
* @return {boolean}
*/
Tag.prototype.isSystemTag = function()
{
	return ('br i pb v'.indexOf(this.name) > -1);
};

/**
* Test whether this tag is a start tag (self-closing tags inclusive)
*
* @return {boolean}
*/
Tag.prototype.isStartTag = function()
{
	return !!(this.type & Tag.START_TAG);
};

/**
* Test whether this tag represents verbatim text
*
* @return {boolean}
*/
Tag.prototype.isVerbatim = function()
{
	return (this.name === 'v');
};

//==========================================================================
// Attributes handling
//==========================================================================

/**
* Return the value of given attribute
*
* @param  {string} attrName
* @return {string}
*/
Tag.prototype.getAttribute = function(attrName)
{
	return this.attributes[attrName];
};

/**
* Return whether given attribute is set
*
* @param  {string} attrName
* @return {boolean}
*/
Tag.prototype.hasAttribute = function(attrName)
{
	return (attrName in this.attributes);
};

/**
* Remove given attribute
*
* @param {string} attrName
*/
Tag.prototype.removeAttribute = function(attrName)
{
	delete this.attributes[attrName];
};

/**
* Set the value of an attribute
*
* @param {string} attrName  Attribute's name
* @param {*}       attrValue Attribute's value
*/
Tag.prototype.setAttribute = function(attrName, attrValue)
{
	this.attributes[attrName] = attrValue;
};

/**
* Set all of this tag's attributes at once
*
* @param {!Object} attributes
*/
Tag.prototype.setAttributes = function(attributes)
{
	this.attributes = {};
	for (let attrName in attributes)
	{
		this.attributes[attrName] = attributes[attrName];
	}
};