/*
Moves the current line or selection up or down

action.setup:
    - direction (string): 'up' or 'down'

TODO:
- Test movement in Python and Ruby
- Test movement in mixed HTML and PHP

I am not sure if it makes sense to continue with this action, because the addition of formatters throws its assumptions about how items are indented in too much question. What if the user wants to use single-line CSS statements, for instance? This logic has no way to handle that.

Leaving it in the repo in case it comes in handy down the road, though. I do really enjoy the behavior, so it would be awesome if we could get it working, maybe by providing a formatter API that shunts the task of re-indenting things off to the formatting subsystem instead of doing everything by hand.
*/

var utils = loadLibrary('tea-utils');
// Used to only allow entry into certain zones
var allowedZones = new SXSelector(':not(js.literal, comment)');

action.titleWithContext = function(context, outError) {
	if (typeof action.setup.direction === 'undefined') {
		return null;
	}
	
	var range = context.selectedRanges[0];
	if (range.length > 0) {
		// Something is selected; check to see if the selection spans multiple lines
		if (context.lineStorage.lineNumberForIndex(range.location) !== context.lineStorage.lineNumberForIndex(range.location + range.length - 1)) {
			return '@multiple';
		}
	}
	return null;
};

action.performWithContext = function(context, outError) {
	// Make sure that there's a direction parameter passed
	if (typeof action.setup.direction === 'undefined') {
		console.log('move_line requires a direction parameter ("up" or "down" are valid)');
		return false;
	}
	
	var direction = action.setup.direction.toLowerCase(),
		// Grab our current selected range
		range = context.selectedRanges[0],
		// Check to see if the selection spans multiple lines
		moveRange, currentLine, multipleLines = false;
	if (range.length > 0) {
		// Grab the range for the lines our selection spans
		moveRange = context.lineStorage.lineRangeForRange(range);
		// Figure out if we are spanning multiple lines
		var nextStartIndex = context.lineStorage.lineStartIndexGreaterThanIndex(moveRange.location);
		if (nextStartIndex < (moveRange.location + moveRange.length)) {
			multipleLines = true;
			if (direction === 'up') {
				currentLine = context.lineStorage.lineNumberForIndex(moveRange.location);
			} else {
				// We have to remove one index from the final number because it either includes a linebreak, or else is at the very end of the document (and you can't select the last line of the document unless there is at least one character there)
				currentLine = context.lineStorage.lineNumberForIndex(moveRange.location + moveRange.length - 1);
			}
		}
	} else {
		moveRange = context.lineStorage.lineRangeForIndex(range.location);
	}
	
	if (!currentLine) {
		// We only don't have a current line if it's a singleton; set to the line for the first index as a result
		currentLine = context.lineStorage.lineNumberForIndex(moveRange.location);
	}
	
	// Grab our target line number and fire up our shared recipe variable and new selection tracker
	var swapLine = (direction === 'up' ? currentLine - 1 : currentLine + 1),
		recipe = new CETextRecipe(),
		selection = false;
	
	// If we are targeting a line outside the document, we need to add new lines
	if (swapLine < 1 || swapLine > context.lineStorage.numberOfLines) {
		var moveText = context.string.substringWithRange(moveRange),
			moveBreak = moveText.replace(/^[\s\S]*?([\n\r]*)$/, '$1'),
			// Construct new line based on indent level of existing line
			swapText = moveText.replace(/^([ \t]*)(?:\S[\s\S]*)?[\n\r]*$/, '$1') + (direction === 'down' ? context.textPreferences.lineEndingString : moveBreak);
		
		// Do our insertions
		if (direction === 'up') {
			if (moveBreak === '') {
				moveText += context.textPreferences.lineEndingString;
			}
			if (moveText.length !== moveRange.length) {
				recipe.replaceRange(moveRange, moveText);
			}
			recipe.insertAtIndex(moveRange.location + moveRange.length, swapText);
			var leadingSpaceLength = moveText.replace(/^((?:\r\n|\r|\n)?[ \t]*)[\s\S]*$/, '$1').length,
				trailingSpaceLength = moveText.replace(/^[\s\S]*?((?:\r\n|\r|\n)[ \t]*|)$/, '$1').length;
			selection = new Range(0 + leadingSpaceLength, moveText.length - leadingSpaceLength - trailingSpaceLength);
		} else {
			if (moveBreak !== '') {
				moveText = moveText.replace(/^([\s\S]*?)[\n\r]*$/, '$1');
			}
			if (moveText.length !== moveRange.length) {
				recipe.replaceRange(moveRange, moveText);
			}
			recipe.insertAtIndex(moveRange.location, swapText);
			var startLocation = moveRange.location + swapText.length,
				leadingSpaceLength = moveText.replace(/^((?:\r\n|\r|\n)?[ \t]*)[\s\S]*$/, '$1').length,
				trailingSpaceLength = moveText.replace(/^[\s\S]*?((?:\r\n|\r|\n)[ \t]*|)$/, '$1').length;
			selection = new Range(startLocation + leadingSpaceLength, moveText.length - leadingSpaceLength - trailingSpaceLength);
		}
	}
	
	// If we aren't moving outside the document boundaries, check for itemizer-based reindentation
	if (selection === false) {
		selection = adjustIndentationForItems(context, direction, moveRange, swapLine, recipe);
	}
	
	// If we aren't doing an itemizer-based re-indentation, then do a standard line swap
	if (selection === false) {
		console.log('standard swap');
		// Grab the range of the target
		var swapRange = context.lineStorage.lineRangeForLineNumber(swapLine),
			// Grab our text
			moveText = context.string.substringWithRange(moveRange),
			swapText = context.string.substringWithRange(swapRange);
		
		if (swapLine == context.lineStorage.numberOfLines || (direction === 'up' && moveRange.location + moveRange.length === context.string.length)) {
			// Swap linebreaks to make sure that the last line of the document doesn't end up appending the selection instead of swapping with it
			var moveBreak = moveText.replace(/^[\s\S]*?([\n\r]*)$/, '$1'),
				swapBreak = swapText.replace(/^[\s\S]*?([\n\r]*)$/, '$1');
			moveText = moveText.replace(/^([\s\S]*?)[\n\r]*$/, '$1' + swapBreak);
			swapText = swapText.replace(/^([\s\S]*?)[\n\r]*$/, '$1' + moveBreak);
		}
		
		// The order of the replacements matters depending on if you are going up or down
		if (direction === 'up') {
			recipe.replaceRange(swapRange, moveText);
			recipe.replaceRange(moveRange, swapText);
			// Create our target selected range
			var startLocation = swapRange.location,
				leadingSpaceLength = moveText.replace(/^((?:\r\n|\r|\n)?[ \t]*)[\s\S]*$/, '$1').length,
				trailingSpaceLength = moveText.replace(/^[\s\S]*?((?:\r\n|\r|\n)[ \t]*|)$/, '$1').length;
			selection = new Range(swapRange.location + leadingSpaceLength, moveText.length - leadingSpaceLength - trailingSpaceLength);
		} else {
			recipe.replaceRange(moveRange, swapText);
			recipe.replaceRange(swapRange, moveText);
			// Create target selected range
			var startLocation = moveRange.location + swapText.length,
				leadingSpaceLength = moveText.replace(/^((?:\r\n|\r|\n)?[ \t]*)[\s\S]*$/, '$1').length,
				trailingSpaceLength = moveText.replace(/^[\s\S]*?((?:\r\n|\r|\n)[ \t]*|)$/, '$1').length;
			selection = new Range(startLocation + leadingSpaceLength, moveText.length - leadingSpaceLength - trailingSpaceLength);
		}
	}
	
	// Apply our recipe!
	context.applyTextRecipe(recipe, CETextOptionVerbatim);
	// Move our selection along with the text
	context.selectedRanges = [selection];
	// Signal success
	return true;
};

/**
 * Returns true if the item ranges are identical (only way I know of to check that they are the same)
 */
function itemsAreEqual(item1, item2) {
	return (item1 === null && item2 === null) || (item1 && item2 && item1.range.location === item2.range.location && item1.range.length === item2.range.length);
}

/**
 * Returns the first empty item it finds in the array, or null if one doesn't exist
 */
function emptyItemInArray(context, inArray, direction) {
	var count = inArray.length,
		i = (direction === 'up' ? count - 1 : 0),
		item;
	while (i >= 0 && i < count) {
		item = inArray[i];
		if (item.range.location + item.range.length !== item.innerRange.location + item.innerRange.length && /^\s*$/.test(context.substringWithRange(item.innerRange))) {
			return item;
		}
		i = (direction === 'up' ? i - 1 : i + 1);
	}
	return null;
}

/**
 * Compares two ranges, and returns an integer representing their relationship
 * 
 * Valid returns:
 * 
 * -2: range1 falls completely before range2
 * -1: range1 overlaps the beginning of range2
 *  0: range1 completely contains range2
 *  1: range1 overlaps end of range2
 *  2: range1 falls completely after range2
 */
function compareRanges(range1, range2) {
	var range1end = range1.location + range1.length,
		range2end = range2.location + range2.length;
	if (range1end <= range2.location) {
		return -2;
	}
	if (range1.location <= range2.location && range1end < range2end) {
		return -1;
	}
	if (range1.location <= range2.location && range1end >= range2end) {
		return 0;
	}
	if (range1.location < range2end && range1end >= range2end) {
		return 1;
	}
	if (range1.location >= range2end) {
		return 2;
	}
}

/**
 * Returns the indentation, normalized for spaces or tabs (based on prefs)
 */
function normalizeIndent(context, indentStr) {
	var useTabs = context.textPreferences.tabString === '\t',
		spaceString = Array(context.textPreferences.numberOfSpacesForTab + 1).join(' ');
	if (useTabs) {
		return indentStr.replace(new RegExp(spaceString), '\t');
	} else {
		return indentStr.replace(/\t/, spaceString);
	}
}

/**
 * Returns the base level of indentation for the lines in the array,
 * and normalizes them for spaces or tabs
 */
function findBaseIndent(context, splitLineArray) {
	var useTabs = context.textPreferences.tabString === '\t',
		curLine, curIndent, shortestIndent = null;
	for (var i = 0, count = splitLineArray.length; i < count; i++) {
		curLine = splitLineArray[i];
		curIndent = normalizeIndent(context, curLine.replace(/^([ \t]*)[\s\S]*$/, '$1'));
		if ((useTabs && /^\t*$/.test(curIndent)) || (!useTabs && /^ *$/.test(curIndent))) {
			if (shortestIndent === null || curIndent.length < shortestIndent) {
				shortestIndent = curIndent;
			}
		}
	}
	if (shortestIndent === null) {
		shortestIndent = '';
	}
	return shortestIndent;
}

/**
 * Adjusts indentation if we are moving into or out of an itemized block
 * 
 * Returns the target selection range, or false
 */
function adjustIndentationForItems(context, direction, moveRange, swapLine, recipe) {
	// Within the document, so check to see if we are moving into an itemized block
	var swapLineRange = context.lineStorage.lineRangeForLineNumber(swapLine),
		curItem = context.itemizer.itemAtCharacterIndex(moveRange.location),
		targetItem = (direction === 'up' ? context.itemizer.itemAtCharacterIndex(swapLineRange.location) : context.itemizer.itemAtCharacterIndex(swapLineRange.location + swapLineRange.length - 1)),
		possibleItems = context.itemizer.itemsInCharacterRange(swapLineRange),
		moveToParent = false, tempTargetItem = null;
	
	// If we have an empty item, target the empty
	if (possibleItems !== null) {
		tempTargetItem = emptyItemInArray(context, possibleItems, direction);
		if (tempTargetItem !== null) {
			targetItem = tempTargetItem;
		}
	}
	
	var passingItemBoundaries = false,
		insideCurItem = false,
		equalItems = itemsAreEqual(curItem, targetItem);
	if (curItem !== null && curItem.innerRange !== null) {
		console.log('curItem.range: ' + curItem.range + '; innerRange: ' + curItem.innerRange);
		if (moveRange.location >= curItem.innerRange.location && moveRange.location + moveRange.length <= curItem.innerRange.location + curItem.innerRange.length /*&& (curItem.range.location !== curItem.innerRange.location || curItem.range.location + curItem.range.length !== curItem.innerRange.location + curItem.innerRange.length)*/) {
			insideCurItem = true;
		}
	}
	
	if (equalItems && curItem !== null && curItem.innerRange !== null) {
		console.log('same item');
		// Since current and target are equal, we need to check if we are moving out of or into the innerRange
		if (direction === 'up') {
			if (insideCurItem && swapLineRange.location < curItem.innerRange.location) {
				// Move up and out of the innerRange
				console.log('up and out of innerRange');
				passingItemBoundaries = true;
				moveToParent = true;
			} else if (!insideCurItem && swapLineRange.location <= curItem.innerRange.location + curItem.innerRange.length) {
				// Move up and into the innerRange
				console.log('up and into innerRange');
				passingItemBoundaries = true;
			}
		} else {
			if (insideCurItem && swapLineRange.location + swapLineRange.length > curItem.innerRange.location + curItem.innerRange.length) {
				// Move down and out of the innerRange
				console.log('down and out of innerRange');
				passingItemBoundaries = true;
				moveToParent = true;
			} else if (!insideCurItem && swapLineRange.location + swapLineRange.length >= curItem.innerRange.location) {
				// Move down and into the innerRange
				console.log('down and into innerRange');
				passingItemBoundaries = true;
			}
		}
	} else if (direction === 'down' && !insideCurItem && curItem !== null && curItem.innerRange !== null && compareRanges(swapLineRange, curItem.innerRange) === 0 && /^\s*$/.test(context.substringWithRange(curItem.innerRange))) {
		// Mainly intended for CSS: if moving downward, the current item's innerRange is completely encapsulated by the next line, and the innerRange is only whitespace then we'll move into the inner range instead of swapping the lines
		console.log('CSS special case: enter empty innerRange');
		targetItem = curItem;
		passingItemBoundaries = true;
	} else if (!equalItems) {
		// Since the two are different, that means we are guaranteed to be passing an item boundary
		passingItemBoundaries = true;
	}
	
	// If we are moving from one item to another, then we might need to adjust indentation
	if (passingItemBoundaries && (targetItem === null || allowedZones.matches(targetItem))) {
		console.log('passing item boundaries');
		// Check if we are moving into the parent item
		var rangeComparison = (curItem !== null && targetItem !== null ? compareRanges(targetItem.range, curItem.range) : null);
		console.log('rangeComparison between targetItem and curItem: ' + rangeComparison);
		console.log('insideCurItem: ' + insideCurItem);
		if (curItem !== null && insideCurItem && (targetItem === null || rangeComparison === 0 || (direction === 'up' && rangeComparison === -1) || (direction === 'down' && rangeComparison === 1))) {
			moveToParent = true;
		}
		
		// Determine where we are inserting things
		var replaceRange;
		if (moveToParent) {
			console.log('moving to parent');
			if (direction === 'up') {
				replaceRange = new Range(curItem.range.location, 0);
			} else if (direction === 'down') {
				replaceRange = new Range(curItem.range.location + curItem.range.length, 0);
			}
		} else if (!insideCurItem && targetItem === null) {
			return false;
		} else {
			if (direction === 'up') {
				replaceRange = new Range(targetItem.innerRange.location + targetItem.innerRange.length, 0);
			} else {
				replaceRange = new Range(targetItem.innerRange.location, 0);
			}
		}
		
		// Parse our line indentation by hand (necessary in order to accurately determine ranges to select, but quite brittle)
		var targetStringLines = context.substringWithRange(moveRange).replace(/^([\s\S]*?)(?:\r\n|\r|\n)?$/, '$1').split(/\r\n|\r|\n/),
			itemIndent = normalizeIndent(context, context.substringWithRange(context.lineStorage.lineRangeForIndex(replaceRange.location)).replace(/^([ \t]*)[\s\S]*$/, '$1')),
			baseIndent = findBaseIndent(context, targetStringLines),
			indentStr = context.textPreferences.tabString,
			linebreakStr = context.textPreferences.lineEndingString,
			sameLineItem = (moveToParent ? false : context.lineStorage.lineNumberForIndex(targetItem.innerRange.location) === context.lineStorage.lineNumberForIndex(targetItem.innerRange.location + targetItem.innerRange.length)),
			targetString = '', line, lineIndent;
		for (var i = 0, count = targetStringLines.length; i < count; i++) {
			line = targetStringLines[i];
			lineIndent = line.replace(/^([ \t]*).*$/, '$1');
			line = line.substr(lineIndent.length);
			lineIndent = normalizeIndent(context, lineIndent);
			if (lineIndent === baseIndent) {
				lineIndent = itemIndent + (moveToParent ? '' : indentStr);
			} else if (lineIndent.length > baseIndent.length) {
				lineIndent = itemIndent + (moveToParent ? '' : indentStr) +  lineIndent.substr(baseIndent.length);
			} else {
				lineIndent = itemIndent + (moveToParent ? '' : indentStr);
			}
			if (i === 0) {
				if (sameLineItem || direction === 'down') {
					lineIndent = linebreakStr + itemIndent + (moveToParent ? '' : indentStr);
				} else if (direction === 'up') {
					lineIndent = (moveToParent ? '' : indentStr);
				}
			}
			if (i === count - 1) {
				if (sameLineItem || direction === 'up') {
					linebreakStr = linebreakStr + itemIndent;
				} else if (direction === 'down') {
					linebreakStr = '';
				}
			}
			targetString += lineIndent + line + linebreakStr;
		}
		
		// If the target was previously empty, adjust our replacement range to fill it
		if (!moveToParent) {
			if (/^\s*$/.test(context.substringWithRange(targetItem.innerRange))) {
				console.log('filling empty target');
				replaceRange = targetItem.innerRange;
				if (targetItem.innerRange.length > 0 && /\n|\r/.test(context.substringWithRange(targetItem.innerRange))) {
					if (direction === 'up') {
						targetString = context.textPreferences.lineEndingString + itemIndent + targetString;
					} else {
						targetString += context.textPreferences.lineEndingString + itemIndent;
					}
				}
			}
		}
					
		// Check if removing these lines will result in an empty item, and collapse it, if so
		var skipMoveRange = false;
		if (curItem !== null && insideCurItem && curItem.innerRange !== null) {
			var rootInnerText = String(context.string.substr(curItem.innerRange.location, curItem.innerRange.length)),
				adjustedRangeIndex = moveRange.location - curItem.innerRange.location,
				newInnerText = rootInnerText.substring(0, adjustedRangeIndex) + rootInnerText.substr(adjustedRangeIndex + moveRange.length);
			if (/^\s+$/.test(newInnerText)) {
				console.log('skipMoveRange = true');
				skipMoveRange = true;
			}
		}
		
		// Put together our recipe and new selected text
		// Order matters of recipe operations matters because otherwise undos get super screwy
		var moveOffset = 0;
		if (direction === 'up') {
			if (replaceRange.length) {
				recipe.replaceRange(replaceRange, targetString);
			} else {
				recipe.insertAtIndex(replaceRange.location, targetString);
			}
			if (skipMoveRange && curItem !== null && curItem.innerRange !== null) {
				linebreakStr = context.textPreferences.lineEndingString;
				recipe.replaceRange(curItem.innerRange, linebreakStr + itemIndent + indentStr + linebreakStr + itemIndent);
			} else {
				recipe.deleteRange(moveRange);
			}
		} else {
			if (skipMoveRange && curItem !== null && curItem.innerRange !== null) {
				linebreakStr = context.textPreferences.lineEndingString;
				var spacerString = linebreakStr + itemIndent + indentStr + linebreakStr + itemIndent;
				recipe.replaceRange(curItem.innerRange, spacerString);
				moveOffset = moveRange.length - spacerString.length;
			} else {
				recipe.deleteRange(moveRange);
				moveOffset = moveRange.length;
			}
			if (replaceRange.length) {
				recipe.replaceRange(replaceRange, targetString);
			} else {
				recipe.insertAtIndex(replaceRange.location, targetString);
			}
		}
		
		// Select the new location of our text (trimming for leading and trailing whitespace to only select the moved lines)
		var startLocation = (moveOffset !== 0 ? replaceRange.location - moveOffset : replaceRange.location),
			leadingSpaceLength = targetString.replace(/^((?:\r\n|\r|\n)?[ \t]*)[\s\S]*$/, '$1').length,
			trailingSpaceLength = targetString.replace(/^[\s\S]*?((?:\r\n|\r|\n)[ \t]*|)$/, '$1').length,
			selection = new Range(startLocation + (!skipMoveRange ? leadingSpaceLength : 0), targetString.length - leadingSpaceLength - trailingSpaceLength);
		return selection;
	}
	
	return false;
}
