/*
 * 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FooEditEngine
{
    /// <summary>
    /// LineBreakMethod列挙体
    /// </summary>
    public enum LineBreakMethod
    {
        /// <summary>
        /// 折り返さない
        /// </summary>
        None = 0,
        /// <summary>
        /// 右端で折り返す
        /// </summary>
        PageBound = 1,
        /// <summary>
        /// 文字数で折り返す
        /// </summary>
        CharUnit = 2
    }

    abstract class ViewBase : IDisposable
    {
        const int SpiltCharCount = 1024;

        protected Document Document;
        protected LineToIndexTable _LayoutLines;
        protected Point2 _Src = new Point2();
        protected Rectangle _Rect;
        protected double _LongestWidth;
        bool _DrawLineNumber;
        LineBreakMethod _LineBreak;
        int _LineBreakCharCount = 80;

        public ViewBase(Document doc, ITextRender r)
        {
            this.Document = doc;
            this.Document.UpdateCalledAlways += new DocumentUpdateEventHandler(doc_Update);
            this._LayoutLines = new LineToIndexTable(this.Document, r);
            this._LayoutLines.SpilitString = new SpilitStringEventHandler(LayoutLines_SpilitStringByChar);
            this.render = r;
            this.render.ChangedRenderResource += new ChangedRenderResourceEventHandler(render_ChangedRenderResource);
            this.render.ChangedRightToLeft += render_ChangedRightToLeft;
            this.SrcChanged += new EventHandler((s, e) => { });
            this.PerformLayouted += new EventHandler((s, e) => { });
            this.PageBoundChanged += new EventHandler((s, e) => { });
        }

        public event EventHandler SrcChanged;

        public event EventHandler PerformLayouted;

        public event EventHandler PageBoundChanged;

        /// <summary>
        /// URLをハイパーリンクとして表示するなら真。そうでないなら偽
        /// </summary>
        public bool UrlMark
        {
            get { return this.LayoutLines.UrlMark; }
            set { this.LayoutLines.UrlMark = value; }
        }

        /// <summary>
        /// テキストレンダラ―
        /// </summary>
        public ITextRender render
        {
            get;
            set;
        }

        /// <summary>
        /// 一ページの高さに収まる行数を返す
        /// </summary>
        public int LineCountOnScreen
        {
            get;
            protected set;
        }

        /// <summary>
        /// 折り返し時の右マージン
        /// </summary>
        public double LineBreakingMarginWidth
        {
            get;
            protected set;
        }

        /// <summary>
        /// 保持しているレイアウト行
        /// </summary>
        public LineToIndexTable LayoutLines
        {
            get { return this._LayoutLines; }
        }

        /// <summary>
        /// 最も長い行の幅
        /// </summary>
        public double LongestWidth
        {
            get { return this._LongestWidth; }
        }

        /// <summary>
        /// 桁折り処理の方法を指定する
        /// </summary>
        /// <remarks>
        /// 変更した場合、呼び出し側で再描写とレイアウトの再構築を行う必要があります
        /// </remarks>
        public LineBreakMethod LineBreak
        {
            get
            {
                return this._LineBreak;
            }
            set
            {
                this._LineBreak = value;
                if (value != LineBreakMethod.None)
                    this._LayoutLines.SpilitString = new SpilitStringEventHandler(LayoutLines_SpilitStringByPixelbase);
                else
                    this._LayoutLines.SpilitString = new SpilitStringEventHandler(LayoutLines_SpilitStringByChar);
            }
        }

        /// <summary>
        /// 折り返し行う文字数。実際に折り返しが行われる幅はem単位×この値となります
        /// </summary>
        public int LineBreakCharCount
        {
            get
            {
                return this._LineBreakCharCount;
            }
            set
            {
                this._LineBreakCharCount = value;
            }
        }

        /// <summary>
        /// シンタックスハイライター
        /// </summary>
        public IHilighter Hilighter
        {
            get { return this._LayoutLines.Hilighter; }
            set { this._LayoutLines.Hilighter = value; }
        }

        /// <summary>
        /// タブの幅
        /// </summary>
        /// <remarks>変更した場合、呼び出し側で再描写する必要があります</remarks>
        public int TabStops
        {
            get { return this.render.TabWidthChar; }
            set { this.render.TabWidthChar = value; }
        }

        /// <summary>
        /// すべてのレイアウト行を破棄し、再度レイアウトをやり直す
        /// </summary>
        public virtual void PerfomLayouts()
        {
            this.doc_Update(this.Document, new DocumentUpdateEventArgs(UpdateType.Clear, -1, -1, -1));
            this.doc_Update(this.Document, new DocumentUpdateEventArgs(UpdateType.Replace, 0, 0, this.Document.Length));
            CalculateLineCountOnScreen();
            this.PerformLayouted(this, null);
        }

        /// <summary>
        /// ページ全体を表す領域
        /// </summary>
        public Rectangle PageBound
        {
            get { return this._Rect; }
            set
            {
                if (value.Width < 0 || value.Height < 0)
                    throw new ArgumentOutOfRangeException("");
                this._Rect = value;
                CalculateClipRect();
                CalculateLineCountOnScreen();
                if (this.render.RightToLeft)
                    this._LayoutLines.ClearLayoutCache();
                this.PageBoundChanged(this, null);
            }
        }

        /// <summary>
        /// Draw()の対象となる領域の左上を表す
        /// </summary>
        public Point2 Src
        {
            get { return this._Src; }
            set { this._Src = value; }
        }

        /// <summary>
        /// 行番号を表示するかどうか
        /// </summary>
        public bool DrawLineNumber
        {
            get { return this._DrawLineNumber; }
            set
            {
                this._DrawLineNumber = value;
                this._LayoutLines.ClearLayoutCache();
                CalculateClipRect();
            }
        }

        public virtual void Draw(Rectangle updateRect)
        {
        }

        public virtual bool TryScroll(double x, int row)
        {
            if (row < 0)
                return true;
            if (row > this.LayoutLines.Count - 1)
                return true;
            this._Src.X = x;
            this._Src.Row = row;
            CalculateLineCountOnScreen();
            this.SrcChanged(this,null);
            return false;
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        public virtual void CalculateLineCountOnScreen()
        {
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                this.Document.UpdateCalledAlways -= new DocumentUpdateEventHandler(this.doc_Update);    //これをしないと複数のビューを作成した時に妙なエラーが発生する
            }
            this._LayoutLines.Clear();
        }

        protected virtual void CalculateClipRect()
        {
        }


        protected virtual void OnSrcChanged(EventArgs e)
        {
            EventHandler handler = this.SrcChanged;
            if (handler != null)
                this.SrcChanged(this, e);
        }

        protected virtual void OnPerformLayoutedChanged(EventArgs e)
        {
            EventHandler handler = this.PerformLayouted;
            if (handler != null)
                this.PerformLayouted(this, e);
        }

        protected virtual void OnPageBoundChanged(EventArgs e)
        {
            EventHandler handler = this.PageBoundChanged;
            if (handler != null)
                this.PageBoundChanged(this, e);
        }

        void render_ChangedRightToLeft(object sender, EventArgs e)
        {
            this._Src.X = 0;
            this._LayoutLines.ClearLayoutCache();
            this.CalculateClipRect();
        }

        void render_ChangedRenderResource(object sender, ChangedRenderRsourceEventArgs e)
        {
            this._LayoutLines.ClearLayoutCache();
            if (e.type == ResourceType.Font)
            {
                this.CalculateClipRect();
                this.CalculateLineCountOnScreen();
            }
        }

        void doc_Update(object sender, DocumentUpdateEventArgs e)
        {
            switch (e.type)
            {
                case UpdateType.Replace:
                    this._LayoutLines.UpdateAsReplace(e.startIndex, e.removeLength, e.insertLength);
                    break;
                case UpdateType.Clear:
                    this._LayoutLines.Clear();
                    this._LongestWidth = 0;
                    break;
            }
        }

        IList<LineToIndexTableData> LayoutLines_SpilitStringByPixelbase(object sender, SpilitStringEventArgs e)
        {
            double WrapWidth;
            if (_LineBreak == LineBreakMethod.PageBound)
                WrapWidth = this.render.TextArea.Width - LineBreakingMarginWidth;  //余白を残さないと欠ける
            else
                WrapWidth = this.render.emSize.Width * this._LineBreakCharCount;

            if (WrapWidth < 0 && this._LineBreak != LineBreakMethod.None)
                throw new InvalidOperationException();

            int startIndex = e.index;
            int endIndex = e.index + e.length - 1;

            LineToIndexTable layoutLineCollection = (LineToIndexTable)sender;

            return this.render.BreakLine(e.buffer,layoutLineCollection, startIndex, endIndex, WrapWidth);
        }

        IList<LineToIndexTableData> LayoutLines_SpilitStringByChar(object sender, SpilitStringEventArgs e)
        {
            LineToIndexTable layoutLineCollection = (LineToIndexTable)sender;
            int startIndex = e.index;
            int endIndex = e.index + e.length - 1;
            List<LineToIndexTableData> output = new List<LineToIndexTableData>();

            foreach (Tuple<int, int> range in this.Document.ForEachLines(startIndex, endIndex, 1000))
            {
                int lineHeadIndex = range.Item1;
                int lineLength = range.Item2;
                char c = this.Document[lineHeadIndex + lineLength - 1];
                bool hasNewLine = c == Document.NewLine;
                output.Add(layoutLineCollection.CreateLineToIndexTableData(lineHeadIndex, lineLength, hasNewLine, null));
            }

            if (output.Count > 0)
                output.Last().LineEnd = true;

            return output;
        }
    }
}
