// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;

using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;

namespace ICSharpCode.AvalonEdit
{
	/// <summary>
	/// The text editor control.
	/// Contains a scrollable TextArea.
	/// </summary>
	[Localizability(LocalizationCategory.Text), ContentProperty("Text")]
	public class TextEditor : Control, ITextEditorComponent, IServiceProvider, IWeakEventListener
	{
		#region Constructors
		static TextEditor()
		{
			DefaultStyleKeyProperty.OverrideMetadata(typeof(TextEditor),
			                                         new FrameworkPropertyMetadata(typeof(TextEditor)));
			FocusableProperty.OverrideMetadata(typeof(TextEditor),
			                                   new FrameworkPropertyMetadata(Boxes.True));
		}
		
		/// <summary>
		/// Creates a new TextEditor instance.
		/// </summary>
		public TextEditor() : this(new TextArea())
		{
		}
		
		/// <summary>
		/// Creates a new TextEditor instance.
		/// </summary>
		protected TextEditor(TextArea textArea)
		{
			if (textArea == null)
				throw new ArgumentNullException("textArea");
			this.textArea = textArea;
			
			textArea.TextView.Services.AddService(typeof(TextEditor), this);
			
			SetCurrentValue(OptionsProperty, textArea.Options);
			SetCurrentValue(DocumentProperty, new TextDocument());
		}
		
		#if !DOTNET4
		void SetCurrentValue(DependencyProperty property, object value)
		{
			SetValue(property, value);
		}
		#endif
		#endregion
		
		/// <inheritdoc/>
		protected override System.Windows.Automation.Peers.AutomationPeer OnCreateAutomationPeer()
		{
			return new TextEditorAutomationPeer(this);
		}
		
		/// Forward focus to TextArea.
		/// <inheritdoc/>
		protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
		{
			base.OnGotKeyboardFocus(e);
			if (e.NewFocus == this) {
				Keyboard.Focus(this.TextArea);
				e.Handled = true;
			}
		}
		
		#region Document property
		/// <summary>
		/// Document property.
		/// </summary>
		public static readonly DependencyProperty DocumentProperty
			= TextView.DocumentProperty.AddOwner(
				typeof(TextEditor), new FrameworkPropertyMetadata(OnDocumentChanged));
		
		/// <summary>
		/// Gets/Sets the document displayed by the text editor.
		/// This is a dependency property.
		/// </summary>
		public TextDocument Document {
			get { return (TextDocument)GetValue(DocumentProperty); }
			set { SetValue(DocumentProperty, value); }
		}
		
		/// <summary>
		/// Occurs when the document property has changed.
		/// </summary>
		public event EventHandler DocumentChanged;
		
		/// <summary>
		/// Raises the <see cref="DocumentChanged"/> event.
		/// </summary>
		protected virtual void OnDocumentChanged(EventArgs e)
		{
			if (DocumentChanged != null) {
				DocumentChanged(this, e);
			}
		}
		
		static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
		{
			((TextEditor)dp).OnDocumentChanged((TextDocument)e.OldValue, (TextDocument)e.NewValue);
		}
		
		void OnDocumentChanged(TextDocument oldValue, TextDocument newValue)
		{
			if (oldValue != null) {
				TextDocumentWeakEventManager.TextChanged.RemoveListener(oldValue, this);
				PropertyChangedEventManager.RemoveListener(oldValue.UndoStack, this, "IsOriginalFile");
			}
			textArea.Document = newValue;
			if (newValue != null) {
				TextDocumentWeakEventManager.TextChanged.AddListener(newValue, this);
				PropertyChangedEventManager.AddListener(newValue.UndoStack, this, "IsOriginalFile");
			}
			OnDocumentChanged(EventArgs.Empty);
			OnTextChanged(EventArgs.Empty);
		}
		#endregion
		
		#region Options property
		/// <summary>
		/// Options property.
		/// </summary>
		public static readonly DependencyProperty OptionsProperty
			= TextView.OptionsProperty.AddOwner(typeof(TextEditor), new FrameworkPropertyMetadata(OnOptionsChanged));
		
		/// <summary>
		/// Gets/Sets the options currently used by the text editor.
		/// </summary>
		public TextEditorOptions Options {
			get { return (TextEditorOptions)GetValue(OptionsProperty); }
			set { SetValue(OptionsProperty, value); }
		}
		
		/// <summary>
		/// Occurs when a text editor option has changed.
		/// </summary>
		public event PropertyChangedEventHandler OptionChanged;
		
		/// <summary>
		/// Raises the <see cref="OptionChanged"/> event.
		/// </summary>
		protected virtual void OnOptionChanged(PropertyChangedEventArgs e)
		{
			if (OptionChanged != null) {
				OptionChanged(this, e);
			}
		}
		
		static void OnOptionsChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
		{
			((TextEditor)dp).OnOptionsChanged((TextEditorOptions)e.OldValue, (TextEditorOptions)e.NewValue);
		}
		
		void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue)
		{
			if (oldValue != null) {
				PropertyChangedWeakEventManager.RemoveListener(oldValue, this);
			}
			textArea.Options = newValue;
			if (newValue != null) {
				PropertyChangedWeakEventManager.AddListener(newValue, this);
			}
			OnOptionChanged(new PropertyChangedEventArgs(null));
		}
		
		/// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/>
		protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
		{
			if (managerType == typeof(PropertyChangedWeakEventManager)) {
				OnOptionChanged((PropertyChangedEventArgs)e);
				return true;
			} else if (managerType == typeof(TextDocumentWeakEventManager.TextChanged)) {
				OnTextChanged(e);
				return true;
			} else if (managerType == typeof(PropertyChangedEventManager)) {
				return HandleIsOriginalChanged((PropertyChangedEventArgs)e);
			}
			return false;
		}
		
		bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
		{
			return ReceiveWeakEvent(managerType, sender, e);
		}
		#endregion
		
		#region Text property
		/// <summary>
		/// Gets/Sets the text of the current document.
		/// </summary>
		[Localizability(LocalizationCategory.Text), DefaultValue("")]
		public string Text {
			get {
				TextDocument document = this.Document;
				return document != null ? document.Text : string.Empty;
			}
			set {
				TextDocument document = GetDocument();
				document.Text = value ?? string.Empty;
				// after replacing the full text, the caret is positioned at the end of the document
				// - reset it to the beginning.
				this.CaretOffset = 0;
				document.UndoStack.ClearAll();
			}
		}
		
		TextDocument GetDocument()
		{
			TextDocument document = this.Document;
			if (document == null)
				throw ThrowUtil.NoDocumentAssigned();
			return document;
		}
		
		/// <summary>
		/// Occurs when the Text property changes.
		/// </summary>
		public event EventHandler TextChanged;
		
		/// <summary>
		/// Raises the <see cref="TextChanged"/> event.
		/// </summary>
		protected virtual void OnTextChanged(EventArgs e)
		{
			if (TextChanged != null) {
				TextChanged(this, e);
			}
		}
		#endregion
		
		#region TextArea / ScrollViewer properties
		readonly TextArea textArea;
		ScrollViewer scrollViewer;
		
		/// <summary>
		/// Is called after the template was applied.
		/// </summary>
		public override void OnApplyTemplate()
		{
			base.OnApplyTemplate();
			scrollViewer = (ScrollViewer)Template.FindName("PART_ScrollViewer", this);
		}
		
		/// <summary>
		/// Gets the text area.
		/// </summary>
		public TextArea TextArea {
			get {
				return textArea;
			}
		}
		
		/// <summary>
		/// Gets the scroll viewer used by the text editor.
		/// This property can return null if the template has not been applied / does not contain a scroll viewer.
		/// </summary>
		internal ScrollViewer ScrollViewer {
			get { return scrollViewer; }
		}
		
		bool CanExecute(RoutedUICommand command)
		{
			TextArea textArea = this.TextArea;
			if (textArea == null)
				return false;
			else
				return command.CanExecute(null, textArea);
		}
		
		void Execute(RoutedUICommand command)
		{
			TextArea textArea = this.TextArea;
			if (textArea != null)
				command.Execute(null, textArea);
		}
		#endregion
		
		#region Syntax highlighting
		/// <summary>
		/// The <see cref="SyntaxHighlighting"/> property.
		/// </summary>
		public static readonly DependencyProperty SyntaxHighlightingProperty =
			DependencyProperty.Register("SyntaxHighlighting", typeof(IHighlightingDefinition), typeof(TextEditor),
			                            new FrameworkPropertyMetadata(OnSyntaxHighlightingChanged));
		
		
		/// <summary>
		/// Gets/sets the syntax highlighting definition used to colorize the text.
		/// </summary>
		public IHighlightingDefinition SyntaxHighlighting {
			get { return (IHighlightingDefinition)GetValue(SyntaxHighlightingProperty); }
			set { SetValue(SyntaxHighlightingProperty, value); }
		}
		
		IVisualLineTransformer colorizer;
		
		static void OnSyntaxHighlightingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			((TextEditor)d).OnSyntaxHighlightingChanged(e.NewValue as IHighlightingDefinition);
		}
		
		void OnSyntaxHighlightingChanged(IHighlightingDefinition newValue)
		{
			if (colorizer != null) {
				this.TextArea.TextView.LineTransformers.Remove(colorizer);
				colorizer = null;
			}
			if (newValue != null) {
				colorizer = CreateColorizer(newValue);
				if (colorizer != null)
					this.TextArea.TextView.LineTransformers.Insert(0, colorizer);
			}
		}
		
		/// <summary>
		/// Creates the highlighting colorizer for the specified highlighting definition.
		/// Allows derived classes to provide custom colorizer implementations for special highlighting definitions.
		/// </summary>
		/// <returns></returns>
		protected virtual IVisualLineTransformer CreateColorizer(IHighlightingDefinition highlightingDefinition)
		{
			if (highlightingDefinition == null)
				throw new ArgumentNullException("highlightingDefinition");
			return new HighlightingColorizer(highlightingDefinition);
		}
		#endregion
		
		#region WordWrap
		/// <summary>
		/// Word wrap dependency property.
		/// </summary>
		public static readonly DependencyProperty WordWrapProperty =
			DependencyProperty.Register("WordWrap", typeof(bool), typeof(TextEditor),
			                            new FrameworkPropertyMetadata(Boxes.False));
		
		/// <summary>
		/// Specifies whether the text editor uses word wrapping.
		/// </summary>
		/// <remarks>
		/// Setting WordWrap=true has the same effect as setting HorizontalScrollBarVisibility=Disabled and will override the
		/// HorizontalScrollBarVisibility setting.
		/// </remarks>
		public bool WordWrap {
			get { return (bool)GetValue(WordWrapProperty); }
			set { SetValue(WordWrapProperty, Boxes.Box(value)); }
		}
		#endregion
		
		#region IsReadOnly
		/// <summary>
		/// IsReadOnly dependency property.
		/// </summary>
		public static readonly DependencyProperty IsReadOnlyProperty =
			DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(TextEditor),
			                            new FrameworkPropertyMetadata(Boxes.False, OnIsReadOnlyChanged));
		
		/// <summary>
		/// Specifies whether the user can change the text editor content.
		/// Setting this property will replace the
		/// <see cref="Editing.TextArea.ReadOnlySectionProvider">TextArea.ReadOnlySectionProvider</see>.
		/// </summary>
		public bool IsReadOnly {
			get { return (bool)GetValue(IsReadOnlyProperty); }
			set { SetValue(IsReadOnlyProperty, Boxes.Box(value)); }
		}
		
		static void OnIsReadOnlyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			TextEditor editor = d as TextEditor;
			if (editor != null) {
				if ((bool)e.NewValue)
					editor.TextArea.ReadOnlySectionProvider = ReadOnlySectionDocument.Instance;
				else
					editor.TextArea.ReadOnlySectionProvider = NoReadOnlySections.Instance;
				
				TextEditorAutomationPeer peer = TextEditorAutomationPeer.FromElement(editor) as TextEditorAutomationPeer;
				if (peer != null) {
					peer.RaiseIsReadOnlyChanged((bool)e.OldValue, (bool)e.NewValue);
				}
			}
		}
		#endregion
		
		#region IsModified
		/// <summary>
		/// Dependency property for <see cref="IsModified"/>
		/// </summary>
		public static readonly DependencyProperty IsModifiedProperty =
			DependencyProperty.Register("IsModified", typeof(bool), typeof(TextEditor),
			                            new FrameworkPropertyMetadata(Boxes.False, OnIsModifiedChanged));
		
		/// <summary>
		/// Gets/Sets the 'modified' flag.
		/// </summary>
		public bool IsModified {
			get { return (bool)GetValue(IsModifiedProperty); }
			set { SetValue(IsModifiedProperty, Boxes.Box(value)); }
		}
		
		static void OnIsModifiedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			TextEditor editor = d as TextEditor;
			if (editor != null) {
				TextDocument document = editor.Document;
				if (document != null) {
					UndoStack undoStack = document.UndoStack;
					if ((bool)e.NewValue) {
						if (undoStack.IsOriginalFile)
							undoStack.DiscardOriginalFileMarker();
					} else {
						undoStack.MarkAsOriginalFile();
					}
				}
			}
		}
		
		bool HandleIsOriginalChanged(PropertyChangedEventArgs e)
		{
			if (e.PropertyName == "IsOriginalFile") {
				TextDocument document = this.Document;
				if (document != null) {
					SetCurrentValue(IsModifiedProperty, Boxes.Box(!document.UndoStack.IsOriginalFile));
				}
				return true;
			} else {
				return false;
			}
		}
		#endregion
		
		#region ShowLineNumbers
		/// <summary>
		/// ShowLineNumbers dependency property.
		/// </summary>
		public static readonly DependencyProperty ShowLineNumbersProperty =
			DependencyProperty.Register("ShowLineNumbers", typeof(bool), typeof(TextEditor),
			                            new FrameworkPropertyMetadata(Boxes.False, OnShowLineNumbersChanged));
		
		/// <summary>
		/// Specifies whether line numbers are shown on the left to the text view.
		/// </summary>
		public bool ShowLineNumbers {
			get { return (bool)GetValue(ShowLineNumbersProperty); }
			set { SetValue(ShowLineNumbersProperty, Boxes.Box(value)); }
		}
		
		static void OnShowLineNumbersChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			TextEditor editor = (TextEditor)d;
			var leftMargins = editor.TextArea.LeftMargins;
			if ((bool)e.NewValue) {
				LineNumberMargin lineNumbers = new LineNumberMargin();
				Line line = (Line)DottedLineMargin.Create();
				leftMargins.Insert(0, lineNumbers);
				leftMargins.Insert(1, line);
				var lineNumbersForeground = new Binding("LineNumbersForeground") { Source = editor };
				line.SetBinding(Line.StrokeProperty, lineNumbersForeground);
				lineNumbers.SetBinding(Control.ForegroundProperty, lineNumbersForeground);
			} else {
				for (int i = 0; i < leftMargins.Count; i++) {
					if (leftMargins[i] is LineNumberMargin) {
						leftMargins.RemoveAt(i);
						if (i < leftMargins.Count && DottedLineMargin.IsDottedLineMargin(leftMargins[i])) {
							leftMargins.RemoveAt(i);
						}
						break;
					}
				}
			}
		}
		#endregion
		
		#region LineNumbersForeground
		/// <summary>
		/// LineNumbersForeground dependency property.
		/// </summary>
		public static readonly DependencyProperty LineNumbersForegroundProperty =
			DependencyProperty.Register("LineNumbersForeground", typeof(Brush), typeof(TextEditor),
			                            new FrameworkPropertyMetadata(Brushes.Gray, OnLineNumbersForegroundChanged));
		
		/// <summary>
		/// Gets/sets the Brush used for displaying the foreground color of line numbers.
		/// </summary>
		public Brush LineNumbersForeground {
			get { return (Brush)GetValue(LineNumbersForegroundProperty); }
			set { SetValue(LineNumbersForegroundProperty, value); }
		}
		
		static void OnLineNumbersForegroundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			TextEditor editor = (TextEditor)d;
			var lineNumberMargin = editor.TextArea.LeftMargins.FirstOrDefault(margin => margin is LineNumberMargin) as LineNumberMargin;;
			
			if (lineNumberMargin != null) {
				lineNumberMargin.SetValue(Control.ForegroundProperty, e.NewValue);
			}
		}
		#endregion
		
		#region TextBoxBase-like methods
		/// <summary>
		/// Appends text to the end of the document.
		/// </summary>
		public void AppendText(string textData)
		{
			var document = GetDocument();
			document.Insert(document.TextLength, textData);
		}
		
		/// <summary>
		/// Begins a group of document changes.
		/// </summary>
		public void BeginChange()
		{
			GetDocument().BeginUpdate();
		}
		
		/// <summary>
		/// Copies the current selection to the clipboard.
		/// </summary>
		public void Copy()
		{
			Execute(ApplicationCommands.Copy);
		}
		
		/// <summary>
		/// Removes the current selection and copies it to the clipboard.
		/// </summary>
		public void Cut()
		{
			Execute(ApplicationCommands.Cut);
		}
		
		/// <summary>
		/// Begins a group of document changes and returns an object that ends the group of document
		/// changes when it is disposed.
		/// </summary>
		public IDisposable DeclareChangeBlock()
		{
			return GetDocument().RunUpdate();
		}
		
		/// <summary>
		/// Ends the current group of document changes.
		/// </summary>
		public void EndChange()
		{
			GetDocument().EndUpdate();
		}
		
		/// <summary>
		/// Scrolls one line down.
		/// </summary>
		public void LineDown()
		{
			if (scrollViewer != null)
				scrollViewer.LineDown();
		}
		
		/// <summary>
		/// Scrolls to the left.
		/// </summary>
		public void LineLeft()
		{
			if (scrollViewer != null)
				scrollViewer.LineLeft();
		}
		
		/// <summary>
		/// Scrolls to the right.
		/// </summary>
		public void LineRight()
		{
			if (scrollViewer != null)
				scrollViewer.LineRight();
		}
		
		/// <summary>
		/// Scrolls one line up.
		/// </summary>
		public void LineUp()
		{
			if (scrollViewer != null)
				scrollViewer.LineUp();
		}
		
		/// <summary>
		/// Scrolls one page down.
		/// </summary>
		public void PageDown()
		{
			if (scrollViewer != null)
				scrollViewer.PageDown();
		}
		
		/// <summary>
		/// Scrolls one page up.
		/// </summary>
		public void PageUp()
		{
			if (scrollViewer != null)
				scrollViewer.PageUp();
		}
		
		/// <summary>
		/// Scrolls one page left.
		/// </summary>
		public void PageLeft()
		{
			if (scrollViewer != null)
				scrollViewer.PageLeft();
		}
		
		/// <summary>
		/// Scrolls one page right.
		/// </summary>
		public void PageRight()
		{
			if (scrollViewer != null)
				scrollViewer.PageRight();
		}
		
		/// <summary>
		/// Pastes the clipboard content.
		/// </summary>
		public void Paste()
		{
			Execute(ApplicationCommands.Paste);
		}
		
		/// <summary>
		/// Redoes the most recent undone command.
		/// </summary>
		/// <returns>True is the redo operation was successful, false is the redo stack is empty.</returns>
		public bool Redo()
		{
			if (CanExecute(ApplicationCommands.Redo)) {
				Execute(ApplicationCommands.Redo);
				return true;
			}
			return false;
		}
		
		/// <summary>
		/// Scrolls to the end of the document.
		/// </summary>
		public void ScrollToEnd()
		{
			ApplyTemplate(); // ensure scrollViewer is created
			if (scrollViewer != null)
				scrollViewer.ScrollToEnd();
		}
		
		/// <summary>
		/// Scrolls to the start of the document.
		/// </summary>
		public void ScrollToHome()
		{
			ApplyTemplate(); // ensure scrollViewer is created
			if (scrollViewer != null)
				scrollViewer.ScrollToHome();
		}
		
		/// <summary>
		/// Scrolls to the specified position in the document.
		/// </summary>
		public void ScrollToHorizontalOffset(double offset)
		{
			ApplyTemplate(); // ensure scrollViewer is created
			if (scrollViewer != null)
				scrollViewer.ScrollToHorizontalOffset(offset);
		}
		
		/// <summary>
		/// Scrolls to the specified position in the document.
		/// </summary>
		public void ScrollToVerticalOffset(double offset)
		{
			ApplyTemplate(); // ensure scrollViewer is created
			if (scrollViewer != null)
				scrollViewer.ScrollToVerticalOffset(offset);
		}
		
		/// <summary>
		/// Selects the entire text.
		/// </summary>
		public void SelectAll()
		{
			Execute(ApplicationCommands.SelectAll);
		}
		
		/// <summary>
		/// Undoes the most recent command.
		/// </summary>
		/// <returns>True is the undo operation was successful, false is the undo stack is empty.</returns>
		public bool Undo()
		{
			if (CanExecute(ApplicationCommands.Undo)) {
				Execute(ApplicationCommands.Undo);
				return true;
			}
			return false;
		}
		
		/// <summary>
		/// Gets if the most recent undone command can be redone.
		/// </summary>
		public bool CanRedo {
			get { return CanExecute(ApplicationCommands.Redo); }
		}
		
		/// <summary>
		/// Gets if the most recent command can be undone.
		/// </summary>
		public bool CanUndo {
			get { return CanExecute(ApplicationCommands.Undo); }
		}
		
		/// <summary>
		/// Gets the vertical size of the document.
		/// </summary>
		public double ExtentHeight {
			get {
				return scrollViewer != null ? scrollViewer.ExtentHeight : 0;
			}
		}
		
		/// <summary>
		/// Gets the horizontal size of the current document region.
		/// </summary>
		public double ExtentWidth {
			get {
				return scrollViewer != null ? scrollViewer.ExtentWidth : 0;
			}
		}
		
		/// <summary>
		/// Gets the horizontal size of the viewport.
		/// </summary>
		public double ViewportHeight {
			get {
				return scrollViewer != null ? scrollViewer.ViewportHeight : 0;
			}
		}
		
		/// <summary>
		/// Gets the horizontal size of the viewport.
		/// </summary>
		public double ViewportWidth {
			get {
				return scrollViewer != null ? scrollViewer.ViewportWidth : 0;
			}
		}
		
		/// <summary>
		/// Gets the vertical scroll position.
		/// </summary>
		public double VerticalOffset {
			get {
				return scrollViewer != null ? scrollViewer.VerticalOffset : 0;
			}
		}
		
		/// <summary>
		/// Gets the horizontal scroll position.
		/// </summary>
		public double HorizontalOffset {
			get {
				return scrollViewer != null ? scrollViewer.HorizontalOffset : 0;
			}
		}
		#endregion
		
		#region TextBox methods
		/// <summary>
		/// Gets/Sets the selected text.
		/// </summary>
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public string SelectedText {
			get {
				TextArea textArea = this.TextArea;
				// We'll get the text from the whole surrounding segment.
				// This is done to ensure that SelectedText.Length == SelectionLength.
				if (textArea != null && textArea.Document != null && !textArea.Selection.IsEmpty)
					return textArea.Document.GetText(textArea.Selection.SurroundingSegment);
				else
					return string.Empty;
			}
			set {
				if (value == null)
					throw new ArgumentNullException("value");
				TextArea textArea = this.TextArea;
				if (textArea != null && textArea.Document != null) {
					int offset = this.SelectionStart;
					int length = this.SelectionLength;
					textArea.Document.Replace(offset, length, value);
					// keep inserted text selected
					textArea.Selection = SimpleSelection.Create(textArea, offset, offset + value.Length);
				}
			}
		}
		
		/// <summary>
		/// Gets/sets the caret position.
		/// </summary>
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public int CaretOffset {
			get {
				TextArea textArea = this.TextArea;
				if (textArea != null)
					return textArea.Caret.Offset;
				else
					return 0;
			}
			set {
				TextArea textArea = this.TextArea;
				if (textArea != null)
					textArea.Caret.Offset = value;
			}
		}
		
		/// <summary>
		/// Gets/sets the start position of the selection.
		/// </summary>
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public int SelectionStart {
			get {
				TextArea textArea = this.TextArea;
				if (textArea != null) {
					if (textArea.Selection.IsEmpty)
						return textArea.Caret.Offset;
					else
						return textArea.Selection.SurroundingSegment.Offset;
				} else {
					return 0;
				}
			}
			set {
				Select(value, SelectionLength);
			}
		}
		
		/// <summary>
		/// Gets/sets the length of the selection.
		/// </summary>
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public int SelectionLength {
			get {
				TextArea textArea = this.TextArea;
				if (textArea != null && !textArea.Selection.IsEmpty)
					return textArea.Selection.SurroundingSegment.Length;
				else
					return 0;
			}
			set {
				Select(SelectionStart, value);
			}
		}
		
		/// <summary>
		/// Selects the specified text section.
		/// </summary>
		public void Select(int start, int length)
		{
			int documentLength = Document != null ? Document.TextLength : 0;
			if (start < 0 || start > documentLength)
				throw new ArgumentOutOfRangeException("start", start, "Value must be between 0 and " + documentLength);
			if (length < 0 || start + length > documentLength)
				throw new ArgumentOutOfRangeException("length", length, "Value must be between 0 and " + (documentLength - start));
			textArea.Selection = SimpleSelection.Create(textArea, start, start + length);
			textArea.Caret.Offset = start + length;
		}
		
		/// <summary>
		/// Gets the number of lines in the document.
		/// </summary>
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public int LineCount {
			get {
				TextDocument document = this.Document;
				if (document != null)
					return document.LineCount;
				else
					return 1;
			}
		}
		
		/// <summary>
		/// Clears the text.
		/// </summary>
		public void Clear()
		{
			this.Text = string.Empty;
		}
		#endregion
		
		#region Loading from stream
		/// <summary>
		/// Loads the text from the stream, auto-detecting the encoding.
		/// </summary>
		/// <remarks>
		/// This method sets <see cref="IsModified"/> to false.
		/// </remarks>
		public void Load(Stream stream)
		{
			using (StreamReader reader = FileReader.OpenStream(stream, this.Encoding ?? Encoding.UTF8)) {
				this.Text = reader.ReadToEnd();
				SetCurrentValue(EncodingProperty, reader.CurrentEncoding); // assign encoding after ReadToEnd() so that the StreamReader can autodetect the encoding
			}
			SetCurrentValue(IsModifiedProperty, Boxes.False);
		}
		
		/// <summary>
		/// Loads the text from the stream, auto-detecting the encoding.
		/// </summary>
		public void Load(string fileName)
		{
			if (fileName == null)
				throw new ArgumentNullException("fileName");
			using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) {
				Load(fs);
			}
		}
		
		/// <summary>
		/// Encoding dependency property.
		/// </summary>
		public static readonly DependencyProperty EncodingProperty =
			DependencyProperty.Register("Encoding", typeof(Encoding), typeof(TextEditor));
		
		/// <summary>
		/// Gets/sets the encoding used when the file is saved.
		/// </summary>
		/// <remarks>
		/// The <see cref="Load(Stream)"/> method autodetects the encoding of the file and sets this property accordingly.
		/// The <see cref="Save(Stream)"/> method uses the encoding specified in this property.
		/// </remarks>
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public Encoding Encoding {
			get { return (Encoding)GetValue(EncodingProperty); }
			set { SetValue(EncodingProperty, value); }
		}
		
		/// <summary>
		/// Saves the text to the stream.
		/// </summary>
		/// <remarks>
		/// This method sets <see cref="IsModified"/> to false.
		/// </remarks>
		public void Save(Stream stream)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			var encoding = this.Encoding;
			var document = this.Document;
			StreamWriter writer = encoding != null ? new StreamWriter(stream, encoding) : new StreamWriter(stream);
			if (document != null)
				document.WriteTextTo(writer);
			writer.Flush();
			// do not close the stream
			SetCurrentValue(IsModifiedProperty, Boxes.False);
		}
		
		/// <summary>
		/// Saves the text to the file.
		/// </summary>
		public void Save(string fileName)
		{
			if (fileName == null)
				throw new ArgumentNullException("fileName");
			using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None)) {
				Save(fs);
			}
		}
		#endregion
		
		#region MouseHover events
		/// <summary>
		/// The PreviewMouseHover event.
		/// </summary>
		public static readonly RoutedEvent PreviewMouseHoverEvent =
			TextView.PreviewMouseHoverEvent.AddOwner(typeof(TextEditor));
		
		/// <summary>
		/// The MouseHover event.
		/// </summary>
		public static readonly RoutedEvent MouseHoverEvent =
			TextView.MouseHoverEvent.AddOwner(typeof(TextEditor));
		
		
		/// <summary>
		/// The PreviewMouseHoverStopped event.
		/// </summary>
		public static readonly RoutedEvent PreviewMouseHoverStoppedEvent =
			TextView.PreviewMouseHoverStoppedEvent.AddOwner(typeof(TextEditor));
		
		/// <summary>
		/// The MouseHoverStopped event.
		/// </summary>
		public static readonly RoutedEvent MouseHoverStoppedEvent =
			TextView.MouseHoverStoppedEvent.AddOwner(typeof(TextEditor));
		
		
		/// <summary>
		/// Occurs when the mouse has hovered over a fixed location for some time.
		/// </summary>
		public event MouseEventHandler PreviewMouseHover {
			add { AddHandler(PreviewMouseHoverEvent, value); }
			remove { RemoveHandler(PreviewMouseHoverEvent, value); }
		}
		
		/// <summary>
		/// Occurs when the mouse has hovered over a fixed location for some time.
		/// </summary>
		public event MouseEventHandler MouseHover {
			add { AddHandler(MouseHoverEvent, value); }
			remove { RemoveHandler(MouseHoverEvent, value); }
		}
		
		/// <summary>
		/// Occurs when the mouse had previously hovered but now started moving again.
		/// </summary>
		public event MouseEventHandler PreviewMouseHoverStopped {
			add { AddHandler(PreviewMouseHoverStoppedEvent, value); }
			remove { RemoveHandler(PreviewMouseHoverStoppedEvent, value); }
		}
		
		/// <summary>
		/// Occurs when the mouse had previously hovered but now started moving again.
		/// </summary>
		public event MouseEventHandler MouseHoverStopped {
			add { AddHandler(MouseHoverStoppedEvent, value); }
			remove { RemoveHandler(MouseHoverStoppedEvent, value); }
		}
		#endregion
		
		#region ScrollBarVisibility
		/// <summary>
		/// Dependency property for <see cref="HorizontalScrollBarVisibility"/>
		/// </summary>
		public static readonly DependencyProperty HorizontalScrollBarVisibilityProperty = ScrollViewer.HorizontalScrollBarVisibilityProperty.AddOwner(typeof(TextEditor), new FrameworkPropertyMetadata(ScrollBarVisibility.Visible));
		
		/// <summary>
		/// Gets/Sets the horizontal scroll bar visibility.
		/// </summary>
		public ScrollBarVisibility HorizontalScrollBarVisibility {
			get { return (ScrollBarVisibility)GetValue(HorizontalScrollBarVisibilityProperty); }
			set { SetValue(HorizontalScrollBarVisibilityProperty, value); }
		}
		
		/// <summary>
		/// Dependency property for <see cref="VerticalScrollBarVisibility"/>
		/// </summary>
		public static readonly DependencyProperty VerticalScrollBarVisibilityProperty = ScrollViewer.VerticalScrollBarVisibilityProperty.AddOwner(typeof(TextEditor), new FrameworkPropertyMetadata(ScrollBarVisibility.Visible));
		
		/// <summary>
		/// Gets/Sets the vertical scroll bar visibility.
		/// </summary>
		public ScrollBarVisibility VerticalScrollBarVisibility {
			get { return (ScrollBarVisibility)GetValue(VerticalScrollBarVisibilityProperty); }
			set { SetValue(VerticalScrollBarVisibilityProperty, value); }
		}
		#endregion
		
		object IServiceProvider.GetService(Type serviceType)
		{
			return textArea.GetService(serviceType);
		}
		
		/// <summary>
		/// Gets the text view position from a point inside the editor.
		/// </summary>
		/// <param name="point">The position, relative to top left
		/// corner of TextEditor control</param>
		/// <returns>The text view position, or null if the point is outside the document.</returns>
		public TextViewPosition? GetPositionFromPoint(Point point)
		{
			if (this.Document == null)
				return null;
			TextView textView = this.TextArea.TextView;
			return textView.GetPosition(TranslatePoint(point, textView) + textView.ScrollOffset);
		}
		
		/// <summary>
		/// Scrolls to the specified line.
		/// This method requires that the TextEditor was already assigned a size (WPF layout must have run prior).
		/// </summary>
		public void ScrollToLine(int line)
		{
			ScrollTo(line, -1);
		}
		
		/// <summary>
		/// Scrolls to the specified line/column.
		/// This method requires that the TextEditor was already assigned a size (WPF layout must have run prior).
		/// </summary>
		public void ScrollTo(int line, int column)
		{
			const double MinimumScrollPercentage = 0.3;
			
			TextView textView = textArea.TextView;
			TextDocument document = textView.Document;
			if (scrollViewer != null && document != null) {
				if (line < 1)
					line = 1;
				if (line > document.LineCount)
					line = document.LineCount;
				
				IScrollInfo scrollInfo = textView;
				if (!scrollInfo.CanHorizontallyScroll) {
					// Word wrap is enabled. Ensure that we have up-to-date info about line height so that we scroll
					// to the correct position.
					// This avoids that the user has to repeat the ScrollTo() call several times when there are very long lines.
					VisualLine vl = textView.GetOrConstructVisualLine(document.GetLineByNumber(line));
					double remainingHeight = scrollViewer.ViewportHeight / 2;
					while (remainingHeight > 0) {
						DocumentLine prevLine = vl.FirstDocumentLine.PreviousLine;
						if (prevLine == null)
							break;
						vl = textView.GetOrConstructVisualLine(prevLine);
						remainingHeight -= vl.Height;
					}
				}
				
				Point p = textArea.TextView.GetVisualPosition(new TextViewPosition(line, Math.Max(1, column)), VisualYPosition.LineMiddle);
				double verticalPos = p.Y - scrollViewer.ViewportHeight / 2;
				if (Math.Abs(verticalPos - scrollViewer.VerticalOffset) > MinimumScrollPercentage * scrollViewer.ViewportHeight) {
					scrollViewer.ScrollToVerticalOffset(Math.Max(0, verticalPos));
				}
				if (column > 0) {
					if (p.X > scrollViewer.ViewportWidth - Caret.MinimumDistanceToViewBorder * 2) {
						double horizontalPos = Math.Max(0, p.X - scrollViewer.ViewportWidth / 2);
						if (Math.Abs(horizontalPos - scrollViewer.HorizontalOffset) > MinimumScrollPercentage * scrollViewer.ViewportWidth) {
							scrollViewer.ScrollToHorizontalOffset(horizontalPos);
						}
					} else {
						scrollViewer.ScrollToHorizontalOffset(0);
					}
				}
			}
		}
	}
}