using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace AvalonStudio.TextEditor.Document
{
    internal sealed class LineManager
    {
        #region Rebuild

        public void Rebuild()
        {
            // keep the first document line
            var ls = documentLineTree.GetByNumber(1);
            // but mark all other lines as deleted, and detach them from the other nodes
            for (var line = ls.NextLine; line != null; line = line.NextLine)
            {
                line.isDeleted = true;
                line.parent = line.left = line.right = null;
            }
            // Reset the first line to detach it from the deleted lines
            ls.ResetLine();
            var ds = NewLineFinder.NextNewLine(document, 0);
            var lines = new List<DocumentLine>();
            var lastDelimiterEnd = 0;
            while (ds != SimpleSegment.Invalid)
            {
                ls.TotalLength = ds.Offset + ds.Length - lastDelimiterEnd;
                ls.DelimiterLength = ds.Length;
                lastDelimiterEnd = ds.Offset + ds.Length;
                lines.Add(ls);

                ls = new DocumentLine(document);
                ds = NewLineFinder.NextNewLine(document, lastDelimiterEnd);
            }
            ls.TotalLength = document.TextLength - lastDelimiterEnd;
            lines.Add(ls);
            documentLineTree.RebuildTree(lines);
            foreach (var lineTracker in lineTrackers)
                lineTracker.RebuildDocument();
        }

        #endregion Rebuild

        #region SetLineLength

        /// <summary>
        ///     Sets the total line length and checks the delimiter.
        ///     This method can cause line to be deleted when it contains a single '\n' character
        ///     and the previous line ends with '\r'.
        /// </summary>
        /// <returns>
        ///     Usually returns <paramref name="line" />, but if line was deleted due to
        ///     the "\r\n" merge, returns the previous line.
        /// </returns>
        private DocumentLine SetLineLength(DocumentLine line, int newTotalLength)
        {
            //			changedLines.Add(line);
            //			deletedOrChangedLines.Add(line);
            var delta = newTotalLength - line.TotalLength;
            if (delta != 0)
            {
                foreach (var lt in lineTrackers)
                    lt.SetLineLength(line, newTotalLength);
                line.TotalLength = newTotalLength;
                DocumentLineTree.UpdateAfterChildrenChange(line);
            }
            // determine new DelimiterLength
            if (newTotalLength == 0)
            {
                line.DelimiterLength = 0;
            }
            else
            {
                var lineOffset = line.Offset;
                var lastChar = document.GetCharAt(lineOffset + newTotalLength - 1);
                if (lastChar == '\r')
                {
                    line.DelimiterLength = 1;
                }
                else if (lastChar == '\n')
                {
                    if (newTotalLength >= 2 && document.GetCharAt(lineOffset + newTotalLength - 2) == '\r')
                    {
                        line.DelimiterLength = 2;
                    }
                    else if (newTotalLength == 1 && lineOffset > 0 && document.GetCharAt(lineOffset - 1) == '\r')
                    {
                        // we need to join this line with the previous line
                        var previousLine = line.PreviousLine;
                        RemoveLine(line);
                        return SetLineLength(previousLine, previousLine.TotalLength + 1);
                    }
                    else
                    {
                        line.DelimiterLength = 1;
                    }
                }
                else
                {
                    line.DelimiterLength = 0;
                }
            }
            return line;
        }

        #endregion SetLineLength

        #region ChangeComplete

        public void ChangeComplete(DocumentChangeEventArgs e)
        {
            foreach (var lt in lineTrackers)
            {
                lt.ChangeComplete(e);
            }
        }

        #endregion ChangeComplete

        #region Constructor

        private readonly TextDocument document;
        private readonly DocumentLineTree documentLineTree;

        /// <summary>
        ///     A copy of the line trackers. We need a copy so that line trackers may remove themselves
        ///     while being notified (used e.g. by WeakLineTracker)
        /// </summary>
        private ILineTracker[] lineTrackers;

        internal void UpdateListOfLineTrackers()
        {
            lineTrackers = document.LineTrackers.ToArray();
        }

        public LineManager(DocumentLineTree documentLineTree, TextDocument document)
        {
            this.document = document;
            this.documentLineTree = documentLineTree;
            UpdateListOfLineTrackers();

            Rebuild();
        }

        #endregion Constructor

        #region Change events

        /*
		HashSet<DocumentLine> deletedLines = new HashSet<DocumentLine>();
		readonly HashSet<DocumentLine> changedLines = new HashSet<DocumentLine>();
		HashSet<DocumentLine> deletedOrChangedLines = new HashSet<DocumentLine>();

		/// <summary>
		/// Gets the list of lines deleted since the last RetrieveChangedLines() call.
		/// The returned list is unsorted.
		/// </summary>
		public ICollection<DocumentLine> RetrieveDeletedLines()
		{
			var r = deletedLines;
			deletedLines = new HashSet<DocumentLine>();
			return r;
		}

		/// <summary>
		/// Gets the list of lines changed since the last RetrieveChangedLines() call.
		/// The returned list is sorted by line number and does not contain deleted lines.
		/// </summary>
		public List<DocumentLine> RetrieveChangedLines()
		{
			var list = (from line in changedLines
			            where !line.IsDeleted
			            let number = line.LineNumber
			            orderby number
			            select line).ToList();
			changedLines.Clear();
			return list;
		}

		/// <summary>
		/// Gets the list of lines changed since the last RetrieveDeletedOrChangedLines() call.
		/// The returned list is not sorted.
		/// </summary>
		public ICollection<DocumentLine> RetrieveDeletedOrChangedLines()
		{
			var r = deletedOrChangedLines;
			deletedOrChangedLines = new HashSet<DocumentLine>();
			return r;
		}
		 */

        #endregion Change events

        #region Remove

        public void Remove(int offset, int length)
        {
            Debug.Assert(length >= 0);
            if (length == 0) return;
            var startLine = documentLineTree.GetByOffset(offset);
            var startLineOffset = startLine.Offset;

            Debug.Assert(offset < startLineOffset + startLine.TotalLength);
            if (offset > startLineOffset + startLine.Length)
            {
                Debug.Assert(startLine.DelimiterLength == 2);
                // we are deleting starting in the middle of a delimiter

                // remove last delimiter part
                SetLineLength(startLine, startLine.TotalLength - 1);
                // remove remaining text
                Remove(offset, length - 1);
                return;
            }

            if (offset + length < startLineOffset + startLine.TotalLength)
            {
                // just removing a part of this line
                //startLine.RemovedLinePart(ref deferredEventList, offset - startLineOffset, length);
                SetLineLength(startLine, startLine.TotalLength - length);
                return;
            }
            // merge startLine with another line because startLine's delimiter was deleted
            // possibly remove lines in between if multiple delimiters were deleted
            var charactersRemovedInStartLine = startLineOffset + startLine.TotalLength - offset;
            Debug.Assert(charactersRemovedInStartLine > 0);
            //startLine.RemovedLinePart(ref deferredEventList, offset - startLineOffset, charactersRemovedInStartLine);

            var endLine = documentLineTree.GetByOffset(offset + length);
            if (endLine == startLine)
            {
                // special case: we are removing a part of the last line up to the
                // end of the document
                SetLineLength(startLine, startLine.TotalLength - length);
                return;
            }
            var endLineOffset = endLine.Offset;
            var charactersLeftInEndLine = endLineOffset + endLine.TotalLength - (offset + length);
            //endLine.RemovedLinePart(ref deferredEventList, 0, endLine.TotalLength - charactersLeftInEndLine);
            //startLine.MergedWith(endLine, offset - startLineOffset);

            // remove all lines between startLine (excl.) and endLine (incl.)
            var tmp = startLine.NextLine;
            DocumentLine lineToRemove;
            do
            {
                lineToRemove = tmp;
                tmp = tmp.NextLine;
                RemoveLine(lineToRemove);
            } while (lineToRemove != endLine);

            SetLineLength(startLine, startLine.TotalLength - charactersRemovedInStartLine + charactersLeftInEndLine);
        }

        private void RemoveLine(DocumentLine lineToRemove)
        {
            foreach (var lt in lineTrackers)
                lt.BeforeRemoveLine(lineToRemove);
            documentLineTree.RemoveLine(lineToRemove);
            //			foreach (ILineTracker lt in lineTracker)
            //				lt.AfterRemoveLine(lineToRemove);
            //			deletedLines.Add(lineToRemove);
            //			deletedOrChangedLines.Add(lineToRemove);
        }

        #endregion Remove

        #region Insert

        public void Insert(int offset, ITextSource text)
        {
            var line = documentLineTree.GetByOffset(offset);
            var lineOffset = line.Offset;

            Debug.Assert(offset <= lineOffset + line.TotalLength);
            if (offset > lineOffset + line.Length)
            {
                Debug.Assert(line.DelimiterLength == 2);
                // we are inserting in the middle of a delimiter

                // shorten line
                SetLineLength(line, line.TotalLength - 1);
                // add new line
                line = InsertLineAfter(line, 1);
                line = SetLineLength(line, 1);
            }

            var ds = NewLineFinder.NextNewLine(text, 0);
            if (ds == SimpleSegment.Invalid)
            {
                // no newline is being inserted, all text is inserted in a single line
                //line.InsertedLinePart(offset - line.Offset, text.Length);
                SetLineLength(line, line.TotalLength + text.TextLength);
                return;
            }
            //DocumentLine firstLine = line;
            //firstLine.InsertedLinePart(offset - firstLine.Offset, ds.Offset);
            var lastDelimiterEnd = 0;
            while (ds != SimpleSegment.Invalid)
            {
                // split line segment at line delimiter
                var lineBreakOffset = offset + ds.Offset + ds.Length;
                lineOffset = line.Offset;
                var lengthAfterInsertionPos = lineOffset + line.TotalLength - (offset + lastDelimiterEnd);
                line = SetLineLength(line, lineBreakOffset - lineOffset);
                var newLine = InsertLineAfter(line, lengthAfterInsertionPos);
                newLine = SetLineLength(newLine, lengthAfterInsertionPos);

                line = newLine;
                lastDelimiterEnd = ds.Offset + ds.Length;

                ds = NewLineFinder.NextNewLine(text, lastDelimiterEnd);
            }
            //firstLine.SplitTo(line);
            // insert rest after last delimiter
            if (lastDelimiterEnd != text.TextLength)
            {
                //line.InsertedLinePart(0, text.Length - lastDelimiterEnd);
                SetLineLength(line, line.TotalLength + text.TextLength - lastDelimiterEnd);
            }
        }

        private DocumentLine InsertLineAfter(DocumentLine line, int length)
        {
            var newLine = documentLineTree.InsertLineAfter(line, length);
            foreach (var lt in lineTrackers)
                lt.LineInserted(line, newLine);
            return newLine;
        }

        #endregion Insert
    }
}