// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;

namespace ICSharpCode.AvalonEdit.Editing
{
	/// <summary>
	/// Rectangular selection ("box selection").
	/// </summary>
	public sealed class RectangleSelection : Selection
	{
		#region Commands
		/// <summary>
		/// Expands the selection left by one character, creating a rectangular selection.
		/// Key gesture: Alt+Shift+Left
		/// </summary>
		public static readonly RoutedUICommand BoxSelectLeftByCharacter = Command("BoxSelectLeftByCharacter");
		
		/// <summary>
		/// Expands the selection right by one character, creating a rectangular selection.
		/// Key gesture: Alt+Shift+Right
		/// </summary>
		public static readonly RoutedUICommand BoxSelectRightByCharacter = Command("BoxSelectRightByCharacter");
		
		/// <summary>
		/// Expands the selection left by one word, creating a rectangular selection.
		/// Key gesture: Ctrl+Alt+Shift+Left
		/// </summary>
		public static readonly RoutedUICommand BoxSelectLeftByWord = Command("BoxSelectLeftByWord");
		
		/// <summary>
		/// Expands the selection left by one word, creating a rectangular selection.
		/// Key gesture: Ctrl+Alt+Shift+Right
		/// </summary>
		public static readonly RoutedUICommand BoxSelectRightByWord = Command("BoxSelectRightByWord");
		
		/// <summary>
		/// Expands the selection up by one line, creating a rectangular selection.
		/// Key gesture: Alt+Shift+Up
		/// </summary>
		public static readonly RoutedUICommand BoxSelectUpByLine = Command("BoxSelectUpByLine");
		
		/// <summary>
		/// Expands the selection up by one line, creating a rectangular selection.
		/// Key gesture: Alt+Shift+Down
		/// </summary>
		public static readonly RoutedUICommand BoxSelectDownByLine = Command("BoxSelectDownByLine");
		
		/// <summary>
		/// Expands the selection to the start of the line, creating a rectangular selection.
		/// Key gesture: Alt+Shift+Home
		/// </summary>
		public static readonly RoutedUICommand BoxSelectToLineStart = Command("BoxSelectToLineStart");
		
		/// <summary>
		/// Expands the selection to the end of the line, creating a rectangular selection.
		/// Key gesture: Alt+Shift+End
		/// </summary>
		public static readonly RoutedUICommand BoxSelectToLineEnd = Command("BoxSelectToLineEnd");
		
		static RoutedUICommand Command(string name)
		{
			return new RoutedUICommand(name, name, typeof(RectangleSelection));
		}
		#endregion
		
		TextDocument document;
		readonly int startLine, endLine;
		readonly double startXPos, endXPos;
		readonly int topLeftOffset, bottomRightOffset;
		readonly TextViewPosition start, end;
		
		readonly List<SelectionSegment> segments = new List<SelectionSegment>();
		
		#region Constructors
		/// <summary>
		/// Creates a new rectangular selection.
		/// </summary>
		public RectangleSelection(TextArea textArea, TextViewPosition start, TextViewPosition end)
			: base(textArea)
		{
			InitDocument();
			this.startLine = start.Line;
			this.endLine = end.Line;
			this.startXPos = GetXPos(textArea, start);
			this.endXPos = GetXPos(textArea, end);
			CalculateSegments();
			this.topLeftOffset = this.segments.First().StartOffset;
			this.bottomRightOffset = this.segments.Last().EndOffset;
			
			this.start = start;
			this.end = end;
		}
		
		private RectangleSelection(TextArea textArea, int startLine, double startXPos, TextViewPosition end)
			: base(textArea)
		{
			InitDocument();
			this.startLine = startLine;
			this.endLine = end.Line;
			this.startXPos = startXPos;
			this.endXPos = GetXPos(textArea, end);
			CalculateSegments();
			this.topLeftOffset = this.segments.First().StartOffset;
			this.bottomRightOffset = this.segments.Last().EndOffset;
			
			this.start = GetStart();
			this.end = end;
		}
		
		private RectangleSelection(TextArea textArea, TextViewPosition start, int endLine, double endXPos)
			: base(textArea)
		{
			InitDocument();
			this.startLine = start.Line;
			this.endLine = endLine;
			this.startXPos = GetXPos(textArea, start);
			this.endXPos = endXPos;
			CalculateSegments();
			this.topLeftOffset = this.segments.First().StartOffset;
			this.bottomRightOffset = this.segments.Last().EndOffset;
			
			this.start = start;
			this.end = GetEnd();
		}
		
		void InitDocument()
		{
			document = textArea.Document;
			if (document == null)
				throw ThrowUtil.NoDocumentAssigned();
		}
		
		static double GetXPos(TextArea textArea, TextViewPosition pos)
		{
			DocumentLine documentLine = textArea.Document.GetLineByNumber(pos.Line);
			VisualLine visualLine = textArea.TextView.GetOrConstructVisualLine(documentLine);
			int vc = visualLine.ValidateVisualColumn(pos, true);
			TextLine textLine = visualLine.GetTextLine(vc);
			return visualLine.GetTextLineVisualXPosition(textLine, vc);
		}
		
		void CalculateSegments()
		{
			DocumentLine nextLine = document.GetLineByNumber(Math.Min(startLine, endLine));
			do {
				VisualLine vl = textArea.TextView.GetOrConstructVisualLine(nextLine);
				int startVC = vl.GetVisualColumn(new Point(startXPos, 0), true);
				int endVC = vl.GetVisualColumn(new Point(endXPos, 0), true);
				
				int baseOffset = vl.FirstDocumentLine.Offset;
				int startOffset = baseOffset + vl.GetRelativeOffset(startVC);
				int endOffset = baseOffset + vl.GetRelativeOffset(endVC);
				segments.Add(new SelectionSegment(startOffset, startVC, endOffset, endVC));
				
				nextLine = vl.LastDocumentLine.NextLine;
			} while (nextLine != null && nextLine.LineNumber <= Math.Max(startLine, endLine));
		}
		
		TextViewPosition GetStart()
		{
			SelectionSegment segment = (startLine < endLine ? segments.First() : segments.Last());
			if (startXPos < endXPos) {
				return new TextViewPosition(document.GetLocation(segment.StartOffset), segment.StartVisualColumn);
			} else {
				return new TextViewPosition(document.GetLocation(segment.EndOffset), segment.EndVisualColumn);
			}
		}
		
		TextViewPosition GetEnd()
		{
			SelectionSegment segment = (startLine < endLine ? segments.Last() : segments.First());
			if (startXPos < endXPos) {
				return new TextViewPosition(document.GetLocation(segment.EndOffset), segment.EndVisualColumn);
			} else {
				return new TextViewPosition(document.GetLocation(segment.StartOffset), segment.StartVisualColumn);
			}
		}
		#endregion
		
		/// <inheritdoc/>
		public override string GetText()
		{
			StringBuilder b = new StringBuilder();
			foreach (ISegment s in this.Segments) {
				if (b.Length > 0)
					b.AppendLine();
				b.Append(document.GetText(s));
			}
			return b.ToString();
		}
		
		/// <inheritdoc/>
		public override Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition)
		{
			return SetEndpoint(endPosition);
		}
		
		/// <inheritdoc/>
		public override int Length {
			get {
				return this.Segments.Sum(s => s.Length);
			}
		}
		
		/// <inheritdoc/>
		public override bool EnableVirtualSpace {
			get { return true; }
		}
		
		/// <inheritdoc/>
		public override ISegment SurroundingSegment {
			get {
				return new SimpleSegment(topLeftOffset, bottomRightOffset - topLeftOffset);
			}
		}
		
		/// <inheritdoc/>
		public override IEnumerable<SelectionSegment> Segments {
			get { return segments; }
		}
		
		/// <inheritdoc/>
		public override TextViewPosition StartPosition {
			get { return start; }
		}
		
		/// <inheritdoc/>
		public override TextViewPosition EndPosition {
			get { return end; }
		}
		
		/// <inheritdoc/>
		public override bool Equals(object obj)
		{
			RectangleSelection r = obj as RectangleSelection;
			return r != null && r.textArea == this.textArea
				&& r.topLeftOffset == this.topLeftOffset && r.bottomRightOffset == this.bottomRightOffset
				&& r.startLine == this.startLine && r.endLine == this.endLine
				&& r.startXPos == this.startXPos && r.endXPos == this.endXPos;
		}
		
		/// <inheritdoc/>
		public override int GetHashCode()
		{
			return topLeftOffset ^ bottomRightOffset;
		}
		
		/// <inheritdoc/>
		public override Selection SetEndpoint(TextViewPosition endPosition)
		{
			return new RectangleSelection(textArea, startLine, startXPos, endPosition);
		}
		
		int GetVisualColumnFromXPos(int line, double xPos)
		{
			var vl = textArea.TextView.GetOrConstructVisualLine(textArea.Document.GetLineByNumber(line));
			return vl.GetVisualColumn(new Point(xPos, 0), true);
		}
		
		/// <inheritdoc/>
		public override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e)
		{
			TextLocation newStartLocation = textArea.Document.GetLocation(e.GetNewOffset(topLeftOffset, AnchorMovementType.AfterInsertion));
			TextLocation newEndLocation = textArea.Document.GetLocation(e.GetNewOffset(bottomRightOffset, AnchorMovementType.BeforeInsertion));
			
			return new RectangleSelection(textArea,
			                              new TextViewPosition(newStartLocation, GetVisualColumnFromXPos(newStartLocation.Line, startXPos)),
			                              new TextViewPosition(newEndLocation, GetVisualColumnFromXPos(newEndLocation.Line, endXPos)));
		}
		
		/// <inheritdoc/>
		public override void ReplaceSelectionWithText(string newText)
		{
			if (newText == null)
				throw new ArgumentNullException("newText");
			using (textArea.Document.RunUpdate()) {
				TextViewPosition start = new TextViewPosition(document.GetLocation(topLeftOffset), GetVisualColumnFromXPos(startLine, startXPos));
				TextViewPosition end = new TextViewPosition(document.GetLocation(bottomRightOffset), GetVisualColumnFromXPos(endLine, endXPos));
				int insertionLength;
				int totalInsertionLength = 0;
				int firstInsertionLength = 0;
				int editOffset = Math.Min(topLeftOffset, bottomRightOffset);
				TextViewPosition pos;
				if (NewLineFinder.NextNewLine(newText, 0) == SimpleSegment.Invalid) {
					// insert same text into every line
					foreach (SelectionSegment lineSegment in this.Segments.Reverse()) {
						ReplaceSingleLineText(textArea, lineSegment, newText, out insertionLength);
						totalInsertionLength += insertionLength;
						firstInsertionLength = insertionLength;
					}
					
					int newEndOffset = editOffset + totalInsertionLength;
					pos = new TextViewPosition(document.GetLocation(editOffset + firstInsertionLength));
					
					textArea.Selection = new RectangleSelection(textArea, pos, Math.Max(startLine, endLine), GetXPos(textArea, pos));
				} else {
					string[] lines = newText.Split(NewLineFinder.NewlineStrings, segments.Count, StringSplitOptions.None);
					int line = Math.Min(startLine, endLine);
					for (int i = lines.Length - 1; i >= 0; i--) {
						ReplaceSingleLineText(textArea, segments[i], lines[i], out insertionLength);
						firstInsertionLength = insertionLength;
					}
					pos = new TextViewPosition(document.GetLocation(editOffset + firstInsertionLength));
					textArea.ClearSelection();
				}
				textArea.Caret.Position = textArea.TextView.GetPosition(new Point(GetXPos(textArea, pos), textArea.TextView.GetVisualTopByDocumentLine(Math.Max(startLine, endLine)))).GetValueOrDefault();
			}
		}
		
		void ReplaceSingleLineText(TextArea textArea, SelectionSegment lineSegment, string newText, out int insertionLength)
		{
			if (lineSegment.Length == 0) {
				if (newText.Length > 0 && textArea.ReadOnlySectionProvider.CanInsert(lineSegment.StartOffset)) {
					newText = AddSpacesIfRequired(newText, new TextViewPosition(document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn));
					textArea.Document.Insert(lineSegment.StartOffset, newText);
				}
			} else {
				ISegment[] segmentsToDelete = textArea.GetDeletableSegments(lineSegment);
				for (int i = segmentsToDelete.Length - 1; i >= 0; i--) {
					if (i == segmentsToDelete.Length - 1) {
						if (segmentsToDelete[i].Offset == SurroundingSegment.Offset && segmentsToDelete[i].Length == SurroundingSegment.Length) {
							newText = AddSpacesIfRequired(newText, new TextViewPosition(document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn));
						}
						textArea.Document.Replace(segmentsToDelete[i], newText);
					} else {
						textArea.Document.Remove(segmentsToDelete[i]);
					}
				}
			}
			insertionLength = newText.Length;
		}
		
		/// <summary>
		/// Performs a rectangular paste operation.
		/// </summary>
		public static bool PerformRectangularPaste(TextArea textArea, TextViewPosition startPosition, string text, bool selectInsertedText)
		{
			if (textArea == null)
				throw new ArgumentNullException("textArea");
			if (text == null)
				throw new ArgumentNullException("text");
			int newLineCount = text.Count(c => c == '\n'); // TODO might not work in all cases, but single \r line endings are really rare today.
			TextLocation endLocation = new TextLocation(startPosition.Line + newLineCount, startPosition.Column);
			if (endLocation.Line <= textArea.Document.LineCount) {
				int endOffset = textArea.Document.GetOffset(endLocation);
				if (textArea.Selection.EnableVirtualSpace || textArea.Document.GetLocation(endOffset) == endLocation) {
					RectangleSelection rsel = new RectangleSelection(textArea, startPosition, endLocation.Line, GetXPos(textArea, startPosition));
					rsel.ReplaceSelectionWithText(text);
					if (selectInsertedText && textArea.Selection is RectangleSelection) {
						RectangleSelection sel = (RectangleSelection)textArea.Selection;
						textArea.Selection = new RectangleSelection(textArea, startPosition, sel.endLine, sel.endXPos);
					}
					return true;
				}
			}
			return false;
		}
		
		/// <summary>
		/// Gets the name of the entry in the DataObject that signals rectangle selections.
		/// </summary>
		public const string RectangularSelectionDataType = "AvalonEditRectangularSelection";
		
		/// <inheritdoc/>
		public override System.Windows.DataObject CreateDataObject(TextArea textArea)
		{
			var data = base.CreateDataObject(textArea);
			
			MemoryStream isRectangle = new MemoryStream(1);
			isRectangle.WriteByte(1);
			data.SetData(RectangularSelectionDataType, isRectangle, false);
			return data;
		}
		
		/// <inheritdoc/>
		public override string ToString()
		{
			// It's possible that ToString() gets called on old (invalid) selections, e.g. for "change from... to..." debug message
			// make sure we don't crash even when the desired locations don't exist anymore.
			return string.Format("[RectangleSelection {0} {1} {2} to {3} {4} {5}]", startLine, topLeftOffset, startXPos, endLine, bottomRightOffset, endXPos);
		}
	}
}
