var attrName       = config.attrName,
	hasSingleQuote = (text.indexOf("'") >= 0),
	hasDoubleQuote = (text.indexOf('"') >= 0),
	tagName        = config.tagName;

if (typeof config.disableQuotes === 'undefined')
{
	parseSingleQuotes();
	parseSingleQuotePairs();
	parseDoubleQuotePairs();
}
if (typeof config.disableGuillemets === 'undefined')
{
	parseGuillemets();
}
if (typeof config.disableMathSymbols === 'undefined')
{
	parseNotEqualSign();
	parseSymbolsAfterDigits();
	parseFractions();
}
if (typeof config.disablePunctuation === 'undefined')
{
	parseDashesAndEllipses();
}
if (typeof config.disableSymbols === 'undefined')
{
	parseSymbolsInParentheses();
}

/**
* Add a fancy replacement tag
*
* @param  {number} tagPos Position of the tag in the text
* @param  {number} tagLen Length of text consumed by the tag
* @param  {string} chr    Replacement character
* @param  {number=} prio   Tag's priority
* @return {!Tag}
*/
function addTag(tagPos, tagLen, chr, prio)
{
	var tag = addSelfClosingTag(tagName, tagPos, tagLen, prio || 0);
	tag.setAttribute(attrName, chr);

	return tag;
}

/**
* Parse dashes and ellipses
*
* Does en dash –, em dash — and ellipsis …
*/
function parseDashesAndEllipses()
{
	if (text.indexOf('...') < 0 && text.indexOf('--') < 0)
	{
		return;
	}

	var chrs = {
			'--'  : "\u2013",
			'---' : "\u2014",
			'...' : "\u2026"
		},
		regexp = /---?|\.\.\./g,
		m;
	while (m = regexp.exec(text))
	{
		addTag(m.index, m[0].length, chrs[m[0]]);
	}
}

/**
* Parse pairs of double quotes
*
* Does quote pairs “” -- must be done separately to handle nesting
*/
function parseDoubleQuotePairs()
{
	if (hasDoubleQuote)
	{
		parseQuotePairs('"', /(?:^|\W)".+?"(?!\w)/g, "\u201c", "\u201d");
	}
}

/**
* Parse vulgar fractions
*/
function parseFractions()
{
	if (text.indexOf('/') < 0)
	{
		return;
	}

	/** @const */
	var map = {
		'0/3'  : "\u2189",
		'1/10' : "\u2152",
		'1/2'  : "\u00BD",
		'1/3'  : "\u2153",
		'1/4'  : "\u00BC",
		'1/5'  : "\u2155",
		'1/6'  : "\u2159",
		'1/7'  : "\u2150",
		'1/8'  : "\u215B",
		'1/9'  : "\u2151",
		'2/3'  : "\u2154",
		'2/5'  : "\u2156",
		'3/4'  : "\u00BE",
		'3/5'  : "\u2157",
		'3/8'  : "\u215C",
		'4/5'  : "\u2158",
		'5/6'  : "\u215A",
		'5/8'  : "\u215D",
		'7/8'  : "\u215E"
	};

	var m, regexp = /\b(?:0\/3|1\/(?:[2-9]|10)|2\/[35]|3\/[458]|4\/5|5\/[68]|7\/8)\b/g;
	while (m = regexp.exec(text))
	{
		addTag(m.index, m[0].length, map[m[0]]);
	}
}

/**
* Parse guillemets-style quotation marks
*/
function parseGuillemets()
{
	if (text.indexOf('<<') < 0)
	{
		return;
	}

	var m, regexp = /<<( ?)(?! )[^\n<>]*?[^\n <>]\1>>(?!>)/g;
	while (m = regexp.exec(text))
	{
		var left  = addTag(m.index,                   2, "\u00AB"),
			right = addTag(m.index + m[0].length - 2, 2, "\u00BB");

		left.cascadeInvalidationTo(right);
	}
}

/**
* Parse the not equal sign
*
* Supports != and =/=
*/
function parseNotEqualSign()
{
	if (text.indexOf('!=') < 0 && text.indexOf('=/=') < 0)
	{
		return;
	}

	var m, regexp = /\b (?:!|=\/)=(?= \b)/g;
	while (m = regexp.exec(text))
	{
		addTag(m.index + 1, m[0].length - 1, "\u2260");
	}
}

/**
* Parse pairs of quotes
*
* @param {string}  q          ASCII quote character
* @param {!RegExp} regexp     Regexp used to identify quote pairs
* @param {string}  leftQuote  Fancy replacement for left quote
* @param {string}  rightQuote Fancy replacement for right quote
*/
function parseQuotePairs(q, regexp, leftQuote, rightQuote)
{
	var m;
	while (m = regexp.exec(text))
	{
		var left  = addTag(m.index + m[0].indexOf(q), 1, leftQuote),
			right = addTag(m.index + m[0].length - 1, 1, rightQuote);

		// Cascade left tag's invalidation to the right so that if we skip the left quote,
		// the right quote remains untouched
		left.cascadeInvalidationTo(right);
	}
}

/**
* Parse pairs of single quotes
*
* Does quote pairs ‘’ must be done separately to handle nesting
*/
function parseSingleQuotePairs()
{
	if (hasSingleQuote)
	{
		parseQuotePairs("'", /(?:^|\W)'.+?'(?!\w)/g, "\u2018", "\u2019");
	}
}

/**
* Parse single quotes in general
*
* Does apostrophes ’ after a letter or at the beginning of a word or a couple of digits
*/
function parseSingleQuotes()
{
	if (!hasSingleQuote)
	{
		return;
	}

	var m, regexp = /[a-z]'|(?:^|\s)'(?=[a-z]|[0-9]{2})/gi;
	while (m = regexp.exec(text))
	{
		// Give this tag a worse priority than default so that quote pairs take precedence
		addTag(m.index + m[0].indexOf("'"), 1, "\u2019", 10);
	}
}

/**
* Parse symbols found after digits
*
* Does symbols found after a digit:
*  - apostrophe ’ if it's followed by an "s" as in 80's
*  - prime ′ and double prime ″
*  - multiply sign × if it's followed by an optional space and another digit
*/
function parseSymbolsAfterDigits()
{
	if (!hasSingleQuote && !hasDoubleQuote && text.indexOf('x') < 0)
	{
		return;
	}

	/** @const */
	var map = {
		// 80's -- use an apostrophe
		"'s" : "\u2019",
		// 12' or 12" -- use a prime
		"'"  : "\u2032",
		"' " : "\u2032",
		"'x" : "\u2032",
		'"'  : "\u2033",
		'" ' : "\u2033",
		'"x' : "\u2033"
	};

	var m, regexp = /[0-9](?:'s|["']? ?x(?= ?[0-9])|["'])/g;
	while (m = regexp.exec(text))
	{
		// Test for a multiply sign at the end
		if (m[0][m[0].length - 1] === 'x')
		{
			addTag(m.index + m[0].length - 1, 1, "\u00d7");
		}

		// Test for an apostrophe/prime right after the digit
		var str = m[0].substring(1, 3);
		if (map[str])
		{
			addTag(m.index + 1, 1, map[str]);
		}
	}
}

/**
* Parse symbols found in parentheses such as (c)
*
* Does symbols ©, ® and ™
*/
function parseSymbolsInParentheses()
{
	if (text.indexOf('(') < 0)
	{
		return;
	}

	var chrs = {
			'(c)'  : "\u00A9",
			'(r)'  : "\u00AE",
			'(tm)' : "\u2122"
		},
		regexp = /\((?:c|r|tm)\)/gi,
		m;
	while (m = regexp.exec(text))
	{
		addTag(m.index, m[0].length, chrs[m[0].toLowerCase()]);
	}
}