/*
 * Copyright (C) 2013 FooProject
 * * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or (at your option) any later version.

 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Text.RegularExpressions;
using System.Threading;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using Slusser.Collections.Generic;

namespace FooEditEngine
{
    internal interface ITextLayout : IDisposable
    {
        /// <summary>
        /// 文字列の幅
        /// </summary>
        double Width
        {
            get;
        }

        /// <summary>
        /// 文字列の高さ
        /// </summary>
        double Height
        {
            get;
        }

        /// <summary>
        /// Disposeされているなら真を返す
        /// </summary>
        bool Disposed
        {
            get;
        }

        /// <summary>
        /// 破棄すべきなら真。そうでなければ偽
        /// </summary>
        bool Invaild
        {
            get;
        }

        /// <summary>
        /// X座標に対応するインデックスを得る
        /// </summary>
        /// <param name="x">X座標</param>
        /// <returns>インデックス</returns>
        /// <remarks>行番号の幅は考慮されてないのでView以外のクラスは呼び出さないでください</remarks>
        int GetIndexFromX(double x);

        /// <summary>
        /// インデックスに対応する文字の幅を得る
        /// </summary>
        /// <param name="index">インデックス</param>
        /// <returns>文字の幅</returns>
        double GetWidthFromIndex(int index);

        /// <summary>
        /// インデックスに対応するX座標を得る
        /// </summary>
        /// <param name="index">インデックス</param>
        /// <returns>X座標</returns>
        /// <remarks>行頭にEOFが含まれている場合、0が返ります</remarks>
        double GetXFromIndex(int index);

        /// <summary>
        /// 適切な位置にインデックスを調整する
        /// </summary>
        /// <param name="index">インデックス</param>
        /// <param name="flow">真の場合は隣接するクラスターを指すように調整し、
        /// そうでない場合は対応するクラスターの先頭を指すように調整します</param>
        /// <returns>調整後のインデックス</returns>
        int AlignIndexToNearestCluster(int index, AlignDirection flow);
    }

    internal class SpilitStringEventArgs : EventArgs
    {
        public Document buffer;
        public int index;
        public int length;
        public int row;
        public SpilitStringEventArgs(Document buf, int index, int length,int row)
        {
            this.buffer = buf;
            this.index = index;
            this.length = length;
            this.row = row;
        }
    }

    internal struct SyntaxInfo
    {
        public TokenType type;
        public int index;
        public int length;
        public SyntaxInfo(int index, int length, TokenType type)
        {
            this.type = type;
            this.index = index;
            this.length = length;
        }
    }

    internal enum EncloserType
    {
        None,
        Begin,
        Now,
        End,
    }

    internal class LineToIndexTableData : IDisposable
    {
        ITextLayout _layout;
        public int Index;
        public int Length;
        public bool LineEnd;
        public SyntaxInfo[] Syntax;
        public EncloserType EncloserType;
        internal ITextLayout Layout
        {
            get
            {
                if (this._layout != null && this._layout.Invaild)
                {
                    this._layout.Dispose();
                    this._layout = null;
                }
                if (this._layout == null || this._layout.Disposed)
                    this._layout = this.CreateLayout(this.Index, this.Length, this.Syntax);
                return _layout;
            }
        }
        internal Func<int, int,SyntaxInfo[], ITextLayout> CreateLayout;
        public LineToIndexTableData()
        {
        }
        public LineToIndexTableData(int index, int length, bool lineend, SyntaxInfo[] syntax)
        {
            this.Index = index;
            this.Length = length;
            this.LineEnd = lineend;
            this.Syntax = syntax;
            this.EncloserType = EncloserType.None;
        }

        public void Dispose()
        {
            if(this._layout != null)
                this._layout.Dispose();
        }
    }

    internal delegate IList<LineToIndexTableData> SpilitStringEventHandler(object sender, SpilitStringEventArgs e);

    /// <summary>
    /// 行番号とインデックスを相互変換するためのクラス
    /// </summary>
    public sealed class LineToIndexTable : IEnumerable<string>
    {
        const int MaxEntries = 100;
        Queue<ITextLayout> CacheEntries = new Queue<ITextLayout>();
        GapBuffer<LineToIndexTableData> Lines = new GapBuffer<LineToIndexTableData>();
        Document Document;
        bool _UrlMarker,_IsSync;
        Regex urlPattern = new Regex("(http|https|ftp)(:\\/\\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#]+)");
        ITextRender render;

        internal LineToIndexTable(Document buf, ITextRender r)
        {
            this.Document = buf;
            this.Document.Markers.Updated += Markers_Updated;
            this.render = r;
            this.FoldingCollection = new FoldingCollection();
            this._IsSync = true;
        }

        void Markers_Updated(object sender, EventArgs e)
        {
            this.ClearLayoutCache();
        }

        internal SpilitStringEventHandler SpilitString;

        /// <summary>
        /// 行数を返す
        /// </summary>
        public int Count
        {
            get { return this.Lines.Count; }
        }

        /// <summary>
        /// 折り畳み関係の情報を収めたコレクション
        /// </summary>
        public FoldingCollection FoldingCollection
        {
            get;
            private set;
        }

        /// <summary>
        /// シンタックスハイライター
        /// </summary>
        internal IHilighter Hilighter { get; set; }

        internal IFoldingStrategy FoldingStrategy { get; set; }

        /// <summary>
        /// Urlに下線を引くなら真
        /// </summary>
        /// <remarks>変更を反映させるためには再描写する必要があります</remarks>
        internal bool UrlMark
        {
            get
            {
                return this._UrlMarker;
            }
            set
            {
                this._UrlMarker = value;
                this.Document.RemoveMarker(HilightType.Url);
                this.SetUrlMarker(0, this.Count);
            }
        }

        /// <summary>
        /// 保持しているレイアウトキャッシュをクリアーする
        /// </summary>
        public void ClearLayoutCache()
        {
            foreach (ITextLayout data in this.CacheEntries)
            {
                data.Dispose();
            }
            this.CacheEntries.Clear();
        }

        /// <summary>
        /// 行番号に対応する文字列を返します
        /// </summary>
        /// <param name="n"></param>
        /// <returns></returns>
        public string this[int n]
        {
            get
            {
                LineToIndexTableData data = this.Lines[n];
                string str = this.Document.ToString(data.Index, data.Length);

                return str;
            }
        }

        internal void UpdateAsReplace(int index, int removedLength, int insertedLength)
        {
            //削除すべき行の開始位置と終了位置を求める
            int startRow = this.GetLineNumberFromIndex(index);
            if (startRow > 0 && this.Lines[startRow - 1].LineEnd == false)
                startRow--;

            int endRow = this.GetLineNumberFromIndex(index + removedLength);
            while (endRow < this.Lines.Count && this.Lines[endRow].LineEnd == false)
                endRow++;
            if (endRow >= this.Lines.Count)
                endRow = this.Lines.Count - 1;

            //SpilitStringの対象となる範囲を求める
            int HeadIndex = this.GetIndexFromLineNumber(startRow);

            int LastIndex = this.GetIndexFromLineNumber(endRow) + this.GetLengthFromLineNumber(endRow) - 1;

            int fisrtPartLength = index - HeadIndex;

            int secondPartLength = LastIndex - (index + removedLength - 1);

            int analyzeLength = fisrtPartLength + secondPartLength + insertedLength;

            System.Diagnostics.Debug.Assert(analyzeLength <= this.Document.Length - 1 - HeadIndex + 1);

            //行を削除する
            int removeCount = endRow - startRow + 1;
            for (int i = startRow; i < removeCount; i++)
                this.Lines[i].Dispose();

            this.Lines.RemoveRange(startRow, removeCount);

            //行を分割し、削除した位置に挿入する
            SpilitStringEventArgs e = new SpilitStringEventArgs(this.Document, HeadIndex, analyzeLength, startRow);
            IList<LineToIndexTableData> newLines = SpilitString(this, e);
            foreach (LineToIndexTableData data in newLines)
            {
                data.CreateLayout = this.LineToIndexTableData_CreatLayout;
            }

            this.Lines.InsertRange(startRow, newLines);

            //挿入された行以降を更新する
            int deltaLength = insertedLength - removedLength;

            for (int i = startRow + newLines.Count; i < this.Lines.Count; i++)
            {
                this.Lines[i].Index += deltaLength;
            }

            this.FoldingCollection.UpdateData(this.Document, index, insertedLength, removedLength);

            LineToIndexTableData lastLine = this.Lines.Count > 0 ? this.Lines.Last() : null;
            int lastLineIndex = 0;
            if(lastLine != null)
                lastLineIndex = lastLine.Index + lastLine.Length - 1;

            //最終行が削除された場合は追加する
            LineToIndexTableData dummyLine = null;
            if (this.Lines.Count == 0)
            {
                dummyLine = new LineToIndexTableData();
                dummyLine.CreateLayout = this.LineToIndexTableData_CreatLayout;
                this.Lines.Add(dummyLine);
            }
            else
            {
                if (lastLine.Length != 0 && this.Document[lastLineIndex] == Document.NewLine)
                {
                    dummyLine = new LineToIndexTableData(lastLine.Index + lastLine.Length, 0, true, null);
                    dummyLine.CreateLayout = this.LineToIndexTableData_CreatLayout;
                    this.Lines.Add(dummyLine);
                }
            }

            this.Hilight(startRow, newLines.Count);
            
            SetUrlMarker(startRow, newLines.Count);

            if (lastLine != null)
            {
                foreach (FoldingItem foldingData in this.FoldingCollection.GetRange(index, lastLineIndex))
                    if (foldingData.Start == foldingData.End)
                        this.FoldingCollection.Remove(foldingData);
            }

            this._IsSync = false;
        }

        ITextLayout LineToIndexTableData_CreatLayout(int index, int length,SyntaxInfo[] syntax)
        {
            ITextLayout layout;
            if (length == 0)
            {
                layout = this.render.CreateLaytout("", null, null);
            }
            else
            {
                var markerRange = from s in this.Document.Markers.Get(index, length)
                                  let n = Util.ConvertAbsIndexToRelIndex(s, index, length)
                                  select n;
                layout = this.render.CreateLaytout(this.Document.ToString(index, length).TrimEnd(Document.NewLine), syntax, markerRange);
            }

            if (this.CacheEntries.Count > MaxEntries)
            {
                ITextLayout oldItem = this.CacheEntries.Dequeue();
                oldItem.Dispose();
            }
            this.CacheEntries.Enqueue(layout);
            
            return layout;
        }

        /// <summary>
        /// 行番号をインデックスに変換します
        /// </summary>
        /// <param name="row">行番号</param>
        /// <returns>0から始まるインデックスを返す</returns>
        public int GetIndexFromLineNumber(int row)
        {
            if (row < 0 || row > this.Lines.Count)
                throw new ArgumentOutOfRangeException();
            return this.Lines[row].Index;
        }

        /// <summary>
        /// 行の長さを得ます
        /// </summary>
        /// <param name="row">行番号</param>
        /// <returns>行の文字長を返します</returns>
        public int GetLengthFromLineNumber(int row)
        {
            if (row < 0 || row > this.Lines.Count)
                throw new ArgumentOutOfRangeException();
            return this.Lines[row].Length;
        }

        internal ITextLayout GetLayout(int row)
        {
            return this.Lines[row].Layout;
        }

        internal LineToIndexTableData GetData(int row)
        {
            if (row < 0 || row > this.Lines.Count)
                throw new ArgumentOutOfRangeException();
            return this.Lines[row];
        }

        int lastLineNumber;
        /// <summary>
        /// インデックスを行番号に変換します
        /// </summary>
        /// <param name="index">インデックス</param>
        /// <returns>行番号を返します</returns>
        public int GetLineNumberFromIndex(int index)
        {
            if (index < 0)
                throw new ArgumentOutOfRangeException("indexに負の値を設定することはできません");
            
            if (index == 0 && this.Lines.Count > 0)
                return 0;

            LineToIndexTableData line;

            if (lastLineNumber < this.Lines.Count - 1)
            {
                line = this.Lines[lastLineNumber];
                if (index >= line.Index && index < line.Index + line.Length)
                    return lastLineNumber;
            }

            int left = 0, right = this.Lines.Count - 1, mid;
            while (left <= right)
            {
                mid = (left + right) / 2;
                line = this.Lines[mid];
                if (index >= line.Index && index < line.Index + line.Length)
                {
                    lastLineNumber = mid;
                    return mid;
                }
                if (index < line.Index)
                {
                    right = mid - 1;
                }
                else
                {
                    left = mid + 1;
                }
            }

            line = this.Lines.Last();
            if (index >= line.Index && index <= line.Index + line.Length)   //最終行長+1までキャレットが移動する可能性があるので
            {
                lastLineNumber = this.Lines.Count - 1;
                return lastLineNumber;
            }
            
            throw new ArgumentOutOfRangeException("該当する行が見つかりませんでした");
        }

        /// <summary>
        /// インデックスからテキストポイントに変換します
        /// </summary>
        /// <param name="index">インデックス</param>
        /// <returns>TextPoint構造体を返します</returns>
        public TextPoint GetTextPointFromIndex(int index)
        {
            TextPoint tp = new TextPoint();
            tp.row = GetLineNumberFromIndex(index);
            tp.col = index - this.Lines[tp.row].Index;
            Debug.Assert(tp.row < this.Lines.Count && tp.col <= this.Lines[tp.row].Length);
            return tp;
        }

        /// <summary>
        /// テキストポイントからインデックスに変換します
        /// </summary>
        /// <param name="tp">TextPoint構造体</param>
        /// <returns>インデックスを返します</returns>
        public int GetIndexFromTextPoint(TextPoint tp)
        {
            if (tp == TextPoint.Null)
                throw new ArgumentNullException("TextPoint.Null以外の値でなければなりません");
            if(tp.row < 0 || tp.row > this.Lines.Count)
                throw new ArgumentOutOfRangeException("tp.rowが設定できる範囲を超えています");
            if (tp.col < 0 || tp.col > this.Lines[tp.row].Length)
                throw new ArgumentOutOfRangeException("tp.colが設定できる範囲を超えています");
            return this.Lines[tp.row].Index + tp.col;
        }

        /// <summary>
        /// フォールディングを再生成します
        /// </summary>
        /// <param name="force">ドキュメントが更新されていなくても再生成する</param>
        /// <returns>生成された場合は真を返す</returns>
        /// <remarks>デフォルトではドキュメントが更新されている時にだけ再生成されます</remarks>
        public bool GenerateFolding(bool force = false)
        {
            if (force)
                this._IsSync = false;
            if (this.Document.Length == 0 || this._IsSync)
                return false;
            this.GenerateFolding(0, this.Document.Length - 1);
            this._IsSync = true;
            return true;
        }

        void GenerateFolding(int start, int end)
        {
            if (start > end)
                throw new ArgumentException("start <= endである必要があります");
            if (this.FoldingStrategy != null)
            {
                var items = this.FoldingStrategy.AnalyzeDocument(this.Document, start, end)
                    .Where((item) =>
                    {
                        int startRow = this.GetLineNumberFromIndex(item.Start);
                        int endRow = this.GetLineNumberFromIndex(item.End);
                        return startRow != endRow;
                    })
                    .Select((item) => item);
                this.FoldingCollection.AddRange(items);
            }
        }

        /// <summary>
        /// フォールディングをすべて削除します
        /// </summary>
        public void ClearFolding()
        {
            this.FoldingCollection.Clear();
            this._IsSync = false;
        }

        /// <summary>
        /// すべての行に対しシンタックスハイライトを行います
        /// </summary>
        public void HilightAll()
        {
            this.Hilight(0, this.Lines.Count);
            SetUrlMarker(0, this.Lines.Count);
            this.ClearLayoutCache();
        }

        /// <summary>
        /// ハイライト関連の情報をすべて削除します
        /// </summary>
        public void ClearHilight()
        {
            foreach (LineToIndexTableData line in this.Lines)
                line.Syntax = null;
            this.ClearLayoutCache();
        }

        /// <summary>
        /// すべて削除します
        /// </summary>
        internal void Clear()
        {
            this.ClearLayoutCache();
            this.FoldingCollection.Clear();
            this.Lines.Clear();
            LineToIndexTableData dummy = new LineToIndexTableData();
            dummy.CreateLayout = this.LineToIndexTableData_CreatLayout;
            this.Lines.Add(dummy);
        }

        void Hilight(int row, int rowCount)
        {
            if (this.Hilighter == null || rowCount == 0)
                return;

            //シンタックスハイライトの開始行を求める
            int startRow = row;
            EncloserType type = this.Lines[startRow].EncloserType;
            EncloserType prevLineType = startRow > 0 ? this.Lines[startRow - 1].EncloserType : EncloserType.None;
            if (type == EncloserType.Now || type == EncloserType.End ||
                prevLineType == EncloserType.Now || prevLineType == EncloserType.End)
                startRow = SearchStartRow(startRow);
            else if (prevLineType == EncloserType.Begin)
                startRow = startRow - 1;

            //シンタックスハイライトの終了行を求める
            int endRow = row + rowCount - 1;
            type = this.Lines[endRow].EncloserType;
            prevLineType = endRow > 0 ? this.Lines[endRow - 1].EncloserType : EncloserType.None;
            if (type == EncloserType.Begin || type == EncloserType.Now ||
                prevLineType == EncloserType.Begin || prevLineType == EncloserType.Now)
                endRow = SearchEndRow(endRow);
            else if (endRow + 1 <= this.Lines.Count - 1 && this.Lines[endRow + 1].EncloserType == EncloserType.Now)
                endRow = SearchEndRow(endRow + 1);

            //シンタックスハイライトを行う
            bool hasBeginEncloser = false;
            int i;
            for (i = startRow; i <= endRow; i++)
            {
                this.HilightLine(i, ref hasBeginEncloser);
            }

            if (hasBeginEncloser)   //終了エンクロージャーが見つかったかどうか
            {
                for (; i < this.Lines.Count; i++)
                {
                    if (this.HilightLine(i, ref hasBeginEncloser) < 0)
                        break;
                }
            }

            this.Hilighter.Reset();
        }

        private int HilightLine(int row, ref bool hasBeginEncloser)
        {
            //シンタックスハイライトを行う
            List<SyntaxInfo> syntax = new List<SyntaxInfo>();
            string str = this[row];
            int level = this.Hilighter.DoHilight(str, str.Length, (s) =>
            {
                if (s.type == TokenType.None || s.type == TokenType.Control)
                    return;
                if (str[s.index + s.length - 1] == Document.NewLine)
                    s.length--;
                syntax.Add(new SyntaxInfo(s.index, s.length, s.type));
            });

            LineToIndexTableData lineData = this.GetData(row);
            lineData.Syntax = syntax.ToArray();

            if (level > 0 && hasBeginEncloser == false)  //開始エンクロージャー
            {
                lineData.EncloserType = EncloserType.Begin;
                hasBeginEncloser = true;
            }
            else if (level < 0) //終了エンクロージャー
            {
                lineData.EncloserType = EncloserType.End;
                hasBeginEncloser = false;
            }
            else if (hasBeginEncloser) //エンクロージャーの範囲内
                lineData.EncloserType = EncloserType.Now;
            else
                lineData.EncloserType = EncloserType.None;

            return level;
        }

        private int SearchStartRow(int startRow)
        {
            for (startRow--; startRow >= 0; startRow--)
            {
                EncloserType type = this.Lines[startRow].EncloserType;
                if (type == EncloserType.Begin || type == EncloserType.None)
                    return startRow;
            }
            return 0;
        }

        private int SearchEndRow(int startRow)
        {
            for (startRow++ ; startRow < this.Lines.Count; startRow++)
            {
                EncloserType type = this.Lines[startRow].EncloserType;
                if (type == EncloserType.End)
                    return startRow;
            }
            return this.Lines.Count - 1;
        }

        void SetUrlMarker(int row, int count)
        {
            if (this.UrlMark == false)
                return;

            int startRow = row;
            int endRow = row + count - 1;

            for (int i = startRow; i <= endRow; i++)
            {
                Match m = this.urlPattern.Match(this[i]);

                if (m.Success)
                {
                    int lineHeadIndex = this.GetIndexFromLineNumber(i);
                    int start = lineHeadIndex + m.Index;
                    this.Document.RemoveMarker(start, 1);
                    this.Document.Markers.Add(Marker.Create(start, m.Length, HilightType.Url));
                }
            }
        }

        #region IEnumerable<string> メンバー

        /// <summary>
        /// コレクションを反復処理するためのIEnumeratorを返す
        /// </summary>
        /// <returns>IEnumeratorオブジェクト</returns>
        public IEnumerator<string> GetEnumerator()
        {
            for (int i = 0; i < this.Lines.Count; i++)
                yield return this[i];
        }

        #endregion

        #region IEnumerable メンバー

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            for (int i = 0; i < this.Lines.Count; i++)
                yield return this[i];
        }

        #endregion
    }

}
