/**
 * quote_academy.js
 * 
 * Educate those quotation marks!
 * 
 * setup:
 * - target (string): 'straight' (straightens all quotation marks),
 *   'curly' (converts all quotation marks that do not delimit strings
 *   into curly quotes), 'entity' (same as curly, but uses HTML entities)
 */

action.canPerformWithContext = function(context, outError) {
	// Require a target to perform this action
	return !!action.setup.target;
};

var isDouble = function(character) {
	return character === '"' || character === '“' || character === '”';
};

action.performWithContext = function(context, outError) {
	// Grab our common variables
	var target = action.setup.target.toLowerCase(),
		searchRE = (target === 'straight' ? /“|”|‘|’/g : /'|"/g),
		languagePunctuation = new SXSelector('punctuation.definition.begin, punctuation.definition.end, tag.doctype'),
		leadDouble = '"', tailDouble = leadDouble, leadSingle = "'", tailSingle = leadSingle,
		recipe = new CETextRecipe(),
		range, text;
	// Setup our replacement options
	if (target === 'curly') {
		leadDouble = '“';
		tailDouble = '”';
		leadSingle = "‘";
		tailSingle = "’";
	} else if (target === 'entity') {
		leadDouble = '&ldquo;';
		tailDouble = '&rdquo;';
		leadSingle = '&lsquo;';
		tailSingle = '&rsquo;';
	}
	// Parse through our ranges and setup our recipe changes
	for (var i = 0, count = context.selectedRanges.length; i < count; i++) {
		range = context.selectedRanges[i];
		// If we are working with a cursor, default to the whole document
		if (range.length === 0 && count === 1) {
			range = new Range(0, context.string.length);
		}
		text = context.substringWithRange(range);
		// Loop through all quote instances and figure out our replacements
		text.replace(searchRE, function(match, offset, str) {
			var replacement = null,
				fullOffset = offset + range.location,
				charRange = new Range(fullOffset, 1);
			// Make sure we are not working with a punctuation mark that means something in this coding language
			if (!languagePunctuation.matches(context.syntaxTree.zoneAtCharacterIndex(fullOffset))) {
				// Super easy if we're converting to straight quotes
				if (target === 'straight') {
					replacement = (isDouble(match) ? leadDouble : leadSingle);
				} else {
					// Converting to a curly quote, so we need to figure out which direction it should curl
					// Check previous character
					var prev = '';
					if (fullOffset > 0) {
						prev = context.substringWithRange(new Range(fullOffset - 1, 1));
					}
					// We have slightly different logic depending on double vs. single
					if (isDouble(match)) {
						// Allowed leads: -, em dash, /, (, [, {, ‘, ', whitespace
						if (/^[-—\/(\[{‘'\s]?$/.test(prev)) {
							replacement = leadDouble;
						} else {
							replacement = tailDouble;
						}
					} else {
						// Leading singles are tricky, because we want to check for common exceptions; grab the following characters
						var following = '';
						if (fullOffset + 1 < context.string.length) {
							var trailOffset = fullOffset + 1,
								character = context.substringWithRange(new Range(trailOffset, 1));
							while (/^[\da-z']$/i.test(character)) {
								following += character;
								trailOffset++;
								if (trailOffset < context.string.length) {
									character = context.substringWithRange(new Range(trailOffset, 1));
								} else {
									break;
								}
							}
						}
						// Allowed leads: -, em dash, /, (, [, {, “, ", whitespace
						if (/^[-—\/(\[{“"\s]?$/.test(prev) && !/^(?:bout|cause|em|n'|neath|nother|til|tis|twas|tween|twill|twixt|\d{2})$/i.test(following)) {
							// We have a leading single quote that doesn't match any of our exceptions
							replacement = leadSingle;
						} else {
							// Default to a closing single
							replacement = tailSingle;
						}
					}
				}
				if (replacement !== null) {
					recipe.replaceRange(charRange, replacement);
				}
			}
		});
	}
	
	// Run the recipe
	return context.applyTextRecipe(recipe);
};
