using System;
using System.Drawing;
using System.Windows.Forms;
using System.ComponentModel;

namespace Mix.Tool
{
    /// <summary>
    /// タイムラインクラス
    /// </summary>
    [ToolboxItem(true)]
    public class Timeline : System.Windows.Forms.Control
    {
        #region Private const value

        private const int MIN_DISP_TIME_COUNT = 100; //最小表示タイム数
        private const int DEF_DISP_TIME_COUNT = 300; //デフォルト表示タイム数
        private const int MIN_TIME_LINE_INTERVAL = 2; //最小ライン間隔
        private const int DEF_TIME_LINE_INTERVAL = 3; //デフォルトタイムライン間隔
        private const int BASE_UNIT_TIME = 10; //ユニットのベース単位時間
        private const int UNIT_LINE_COUNT = 10; //ユニットのライン数

        #endregion

        #region Public event definition

        /// <summary>
        /// 時間変更イベントパラメータクラス
        /// </summary>
        public class ChangeTimeEventArgs : System.EventArgs
        {
            /// <summary>
            /// 変更タイプ列挙定数
            /// </summary>
            public enum Types
            {
                Init,
                Click,
                Prop,
            }

            /// <summary>
            /// クリックによる変更の
            /// </summary>
            private ChangeTimeEventArgs.Types type;
            private int time;

            /// <summary>
            /// コンストラクタ
            /// </summary>
            /// <param name="_type">変更タイプ</param>
            /// <param name="_time">変更後の時間</param>
            internal ChangeTimeEventArgs(ChangeTimeEventArgs.Types _type, int _time)
            {
                this.type = _type;
                this.time = _time;
            }

            /// <summary>
            /// 変更の種類を取得します
            /// </summary>
            public ChangeTimeEventArgs.Types Type
            {
                get { return this.type; }
            }

            /// <summary>
            /// 変更後の時間を取得します
            /// </summary>
            public int Time
            {
                get { return this.time; }
            }
        };

        /// <summary>
        /// 時間変更デリゲート
        /// </summary>
        /// <param name="e">イベント</param>
        public delegate void ChangeTimeEventHandler(Timeline.ChangeTimeEventArgs e);

        #endregion

        #region Private value

        private System.Windows.Forms.HScrollBar hScrollBar = new HScrollBar();

        private Rectangle paintRect = new Rectangle();

        private int dispTimeCount = DEF_DISP_TIME_COUNT;
        private int timeLineInterval = DEF_TIME_LINE_INTERVAL;
        private bool followCurrentTime = false;

        private int lastTime = 0;
        private int currentTime = 0;
        private int loopStartTime = 0;
        private int loopEndTime = 0;

        private System.Drawing.Pen broadLinePen = new Pen(System.Drawing.SystemColors.ControlDarkDark);
        private System.Drawing.Pen narrowLinePen = new Pen(System.Drawing.SystemColors.ControlDark);
        private System.Drawing.Pen loopStartLinePen = new Pen(System.Drawing.Color.Red);
        private System.Drawing.Pen loopEndLinePen = new Pen(System.Drawing.Color.Blue);
        private System.Drawing.Pen selectedLinePen = new Pen(System.Drawing.Color.SpringGreen);

        private int paintTimeUnit = 0;
        private int paintTimeUnitWidth = 0;
        private int paintLineCount = 0;
        private int paintTimeInterval = 0;
        private int paintTimeCount = 0;

        #endregion

        #region Public property

        /// <summary>
        /// "現在の時間を追尾するかどうかの取得、または設定
        /// </summary>
        [Category("動作")]
        [Description("現在の時間を追尾するかどうかを指定します。")]
        public bool FollowCurrentTime
        {
            get { return this.followCurrentTime; }
            set
            {
                this.followCurrentTime = value;

                if (this.followCurrentTime == true)
                {
                    this.ProcessFollow();
                }
            }
        }

        /// <summary>
        /// 表示する時間数の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("表示する時間数を指定します。")]
        public int DisplayTimeCount
        {
            get { return this.dispTimeCount; }
            set
            {
                this.dispTimeCount = (MIN_DISP_TIME_COUNT <= value) ? value : MIN_DISP_TIME_COUNT;

                this.UpdateHScrollBar();
                this.UpdatePaintParam();
                this.Invalidate();
            }
        }

        /// <summary>
        /// 時間ラインの間隔の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("時間を表すラインの間隔をピクセル単位で指定します。")]
        public int TimeLineInterval
        {
            get { return this.timeLineInterval; }
            set
            {
                this.timeLineInterval = (MIN_TIME_LINE_INTERVAL <= value) ? value : MIN_TIME_LINE_INTERVAL;
                this.paintTimeUnitWidth = UNIT_LINE_COUNT * this.timeLineInterval;

                this.Invalidate();
            }
        }

        /// <summary>
        /// 最終時間の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("最終時間を指定します。")]
        public int LastTime
        {
            get { return this.lastTime; }
            set
            {
                if (value < 0)
                {
                    throw new System.ArgumentException("最終時間は 0 以上でなければなりません。");
                }

                this.lastTime = value;
                this.currentTime = 0;
                this.loopStartTime = 0;
                this.loopEndTime = this.lastTime;

                this.UpdateHScrollBar();
                this.Invalidate();
            }
        }

        /// <summary>
        /// 現在の時間の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("現在の時間を指定します。")]
        public int CurrentTime
        {
            get { return this.currentTime; }
            set
            {
                if ((0 > value) ||
                    (this.lastTime < value))
                {
                    throw new System.ArgumentException("現在の時間は 0～" + this.lastTime + " の間でなければなりません");
                }

                if (this.currentTime != value)
                {
                    this.currentTime = value;

                    if (this.followCurrentTime == true)
                    {
                        this.ProcessFollow();
                    }

                    this.Invalidate();
                    this.InvokeChangeCurrentTime(ChangeTimeEventArgs.Types.Prop);
                }
            }
        }

        /// <summary>
        /// ループの開始時間の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("ループの開始時間を指定します。")]
        public int LoopStartTime
        {
            get { return loopStartTime; }
            set
            {
                if ((0 > value) ||
                    (this.loopEndTime < value))
                {
                    throw new System.ArgumentException("ループ開始時間は " + "0～" + this.loopEndTime + " の間でなければなりません");
                }

                if (this.loopStartTime != value)
                {
                    this.loopStartTime = value;
                    this.Invalidate();

                    this.InvokeChangeLoopStartTime(ChangeTimeEventArgs.Types.Prop);
                }
            }
        }

        /// <summary>
        /// ループの終了時間の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("ループの終了時間を指定します。")]
        public int LoopEndTime
        {
            get { return this.loopEndTime; }
            set
            {
                if ((this.loopStartTime > value) ||
                    (this.lastTime < value))
                {
                    throw new System.ArgumentException("ループ終了時間は " + this.loopStartTime + "～" + this.lastTime + " の間でなければなりません");
                }

                if (this.loopEndTime != value)
                {
                    this.loopEndTime = value;
                    this.Invalidate();

                    this.InvokeChangeLoopEndTime(ChangeTimeEventArgs.Types.Prop);
                }
            }
        }

        /// <summary>
        /// 表示を開始する時間の取得、または設定
        /// </summary>
        [Browsable(false)]
        public int DisplayStartTime
        {
            get { return this.hScrollBar.Value; }
            set
            {
                if (this.followCurrentTime == true)
                {
                    return;
                }

                if (this.hScrollBar.Minimum > value)
                {
                    this.hScrollBar.Value = this.hScrollBar.Minimum;
                }
                else if (this.hScrollBar.Maximum < value)
                {
                    this.hScrollBar.Value = this.hScrollBar.Maximum;
                }
                else
                {
                    this.hScrollBar.Value = value;
                }
            }
        }

        /// <summary>
        /// 表示を終了する時間の取得
        /// </summary>
        [Browsable(false)]
        public int DisplayEndTime
        {
            get
            {
                int ret;

                if (this.paintTimeCount > 0)
                {
                    ret = this.hScrollBar.Value + this.paintTimeCount - 1;
                }
                else
                {
                    ret = this.hScrollBar.Value;
                }

                return ret;
            }
        }

        /// <summary>
        /// 一定間隔のタイムラインの色の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("時間が表示される広い一定間隔のタイムラインの色を指定します。")]
        public System.Drawing.Color BroadLineColor
        {
            get { return this.broadLinePen.Color; }
            set
            {
                this.broadLinePen.Dispose();
                this.broadLinePen = new Pen(value);

                this.Invalidate();
            }
        }

        /// <summary>
        /// 一定間隔のタイムラインの色の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("時間が表示されない狭い一定間隔のラインの色を指定します。")]
        public System.Drawing.Color NarrowLineColor
        {
            get { return this.narrowLinePen.Color; }
            set
            {
                this.narrowLinePen.Dispose();
                this.narrowLinePen = new Pen(value);

                this.Invalidate();
            }
        }

        /// <summary>
        /// ループ開始時間のラインの色の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("ループ開始時間のラインの色を指定します。")]
        public System.Drawing.Color LoopStartLineColor
        {
            get { return this.loopStartLinePen.Color; }
            set
            {
                this.loopStartLinePen.Dispose();
                this.loopStartLinePen = new Pen(value);

                this.Invalidate();
            }
        }

        /// <summary>
        /// ループ終了時間のラインの色の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("ループ終了時間のラインの色を指定します。")]
        public System.Drawing.Color LoopEndLineColor
        {
            get { return this.loopEndLinePen.Color; }
            set
            {
                this.loopEndLinePen.Dispose();
                this.loopEndLinePen = new Pen(value);

                this.Invalidate();
            }
        }

        /// <summary>
        /// 現在選択されている時間のラインの色の取得、または設定
        /// </summary>
        [Category("表示")]
        [Description("現在選択されている時間のラインの色を指定します。")]
        public System.Drawing.Color SelectedLineColor
        {
            get { return this.selectedLinePen.Color; }
            set
            {
                this.selectedLinePen.Dispose();
                this.selectedLinePen = new Pen(value);

                this.Invalidate();
            }
        }

        #endregion

        #region Public event

        /// <summary>
        /// 時間変更イベントの追加、または削除
        /// </summary>
        [Category("イベント")]
        [Description("現在の時間が変更されたときに発生します。")]
        public event Timeline.ChangeTimeEventHandler ChangeCurrentTime = null;

        /// <summary>
        /// ループ開始時間変更イベントの追加、または削除
        /// </summary>
        [Category("イベント")]
        [Description("ループ開始時間が変更されたときに発生します。")]
        public event Timeline.ChangeTimeEventHandler ChangeLoopStartTime = null;

        /// <summary>
        /// ループ終了時間変更イベントの追加、または削除
        /// </summary>
        [Category("イベント")]
        [Description("ループ終了時間が変更されたときに発生します。")]
        public event Timeline.ChangeTimeEventHandler ChangeLoopEndTime = null;

        #endregion

        #region Public method

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public Timeline()
        {
            this.hScrollBar.SmallChange = 1;
            this.hScrollBar.LargeChange = 1;
            this.hScrollBar.Minimum = 0;
            this.hScrollBar.Maximum = 0;
            this.hScrollBar.Value = 0;
            this.hScrollBar.ValueChanged += new EventHandler(hScrollBar_ValueChanged);

            this.paintTimeUnitWidth = UNIT_LINE_COUNT * this.timeLineInterval;

            this.Disposed += new EventHandler(Timeline_Disposed);
        }

        /// <summary>
        /// 現在の時間を中央に来るようにします
        /// </summary>
        public void CenteringCurrentTime()
        {
            if (this.followCurrentTime == true)
            {
                return;
            }

            if ((this.DisplayStartTime > this.currentTime) ||
                (this.DisplayEndTime <= this.currentTime))
            {
                this.ProcessFollow();
            }
        }

        #endregion

        #region Overrride protected method

        protected override void OnCreateControl()
        {
            base.OnCreateControl();

            this.DoubleBuffered = true;
            this.Controls.Add(this.hScrollBar);

            this.InvokeChangeCurrentTime(ChangeTimeEventArgs.Types.Init);
            this.InvokeChangeLoopStartTime(ChangeTimeEventArgs.Types.Init);
            this.InvokeChangeLoopEndTime(ChangeTimeEventArgs.Types.Init);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            int paintStartTime = this.hScrollBar.Value / this.paintTimeInterval * this.paintTimeInterval;
            int paintEndTime = paintStartTime + this.paintTimeCount;
 
            Point p0 = new Point();
            Point p1 = new Point();
            PointF numPoint = new PointF();

            //メモリの描画
            for (int i = 0; i < this.paintLineCount; i++)
            {
                int pos = paintStartTime + i * this.paintTimeInterval;

                bool lastLine = (this.lastTime <= pos);
                bool broadLine = ((pos % this.paintTimeUnit) == 0);

                System.Drawing.Pen linePen = ((lastLine == true) || (broadLine == true)) ? this.broadLinePen : this.narrowLinePen;

                p0.X = this.paintRect.X + i * this.timeLineInterval;
                p0.Y = ((lastLine == true) || (broadLine == true)) ? this.Font.Height + 2 : this.Font.Height + 10;

                p1.X = p0.X;
                p1.Y = this.paintRect.Height;

                if (broadLine == true)
                {
                    string numText = pos.ToString();
                    SizeF numTextSize = e.Graphics.MeasureString(numText, this.Font);

                    numPoint.X = p0.X - ((numTextSize.Width - this.timeLineInterval) / 2);
                    numPoint.Y = 0;

                    e.Graphics.DrawString(pos.ToString(), this.Font, System.Drawing.SystemBrushes.ControlText, numPoint);
                }

                e.Graphics.DrawLine(linePen, p0, p1);

                if (lastLine == true)
                {
                    break;
                }
            }

            //ループ開始時間
            if (GetCurLine(paintStartTime, paintEndTime, this.loopStartTime, ref p0, ref p1) == true)
            {
                e.Graphics.DrawLine(this.loopStartLinePen, p0, p1);
            }

            //ループ終了時間
            if (GetCurLine(paintStartTime, paintEndTime, this.loopEndTime, ref p0, ref p1) == true)
            {
                e.Graphics.DrawLine(this.loopEndLinePen, p0, p1);
            }

            //カーソルの描画
            if (GetCurLine(paintStartTime, paintEndTime, this.currentTime, ref p0, ref p1) == true)
            {
                e.Graphics.DrawLine(this.selectedLinePen, p0, p1);
            }
        }

        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);

            Size ctrlSize = this.Size;
            Size scrlSize = this.hScrollBar.Size;

            //スクロールバーの矩形を更新
            this.hScrollBar.Bounds = new Rectangle(new Point(0, ctrlSize.Height - scrlSize.Height), new Size(ctrlSize.Width, scrlSize.Height));

            //描画矩形を更新
            this.paintRect.X = this.Font.Height;
            this.paintRect.Y = this.hScrollBar.Location.Y;
            this.paintRect.Width = ctrlSize.Width - this.Font.Height * 2;
            this.paintRect.Height = ctrlSize.Height - scrlSize.Height;

            //描画パラメータの更新
            this.UpdatePaintParam();

            //更新
            this.Invalidate();
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);

            if (e.Button == MouseButtons.Left)
            {
                if (this.UpdateTime(e.X) == true)
                {
                    this.Invalidate();
                    this.InvokeChangeCurrentTime(ChangeTimeEventArgs.Types.Click);
                }
            }
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);

            if (e.Button == MouseButtons.Left)
            {
                if (this.UpdateTime(e.X) == true)
                {
                    this.Invalidate();
                    this.InvokeChangeCurrentTime(ChangeTimeEventArgs.Types.Click);
                }
            }
        }

        #endregion

        #region Private method

        private void hScrollBar_ValueChanged(object sender, EventArgs e)
        {
            this.Invalidate();
        }

        private void Timeline_Disposed(object sender, EventArgs e)
        {
            this.broadLinePen.Dispose();
            this.narrowLinePen.Dispose();
            this.selectedLinePen.Dispose();
            this.loopStartLinePen.Dispose();
            this.loopEndLinePen.Dispose();
            this.hScrollBar.Dispose();
        }

        private void InvokeChangeCurrentTime(ChangeTimeEventArgs.Types type)
        {
            if (this.ChangeCurrentTime != null)
            {
                this.ChangeCurrentTime.Invoke(new ChangeTimeEventArgs(type, this.currentTime));
            }
        }

        private void InvokeChangeLoopStartTime(ChangeTimeEventArgs.Types type)
        {
            if (this.ChangeLoopStartTime != null)
            {
                this.ChangeLoopStartTime.Invoke(new ChangeTimeEventArgs(type, this.loopStartTime));
            }
        }

        private void InvokeChangeLoopEndTime(ChangeTimeEventArgs.Types type)
        {
            if (this.ChangeLoopEndTime != null)
            {
                this.ChangeLoopEndTime.Invoke(new ChangeTimeEventArgs(type, this.loopEndTime));
            }
        }

        private void ProcessFollow()
        {
            int leftTime = this.currentTime - this.paintTimeCount / 2;

            if (this.hScrollBar.Minimum > leftTime)
            {
                this.hScrollBar.Value = this.hScrollBar.Minimum;
            }
            else if (this.hScrollBar.Maximum < leftTime)
            {
                this.hScrollBar.Value = this.hScrollBar.Maximum;
            }
            else
            {
                this.hScrollBar.Value = leftTime;
            }
        }

        private bool UpdateTime(int mouseX)
        {
/*            if (this.followCurrentTime == true)
            {
                return false;
            }
*/
            int preTime = this.currentTime;
            int pos = ((mouseX - this.paintRect.X) / this.timeLineInterval);

            if (pos < 0)
            {
                pos = 0;
            }
            else if (this.paintLineCount <= pos)
            {
                pos = this.paintLineCount - 1;
            }

            this.currentTime = this.hScrollBar.Value / this.paintTimeInterval * this.paintTimeInterval + pos * this.paintTimeInterval;
            if (this.lastTime < this.currentTime)
            {
                this.currentTime = this.lastTime;
            }

            return (this.currentTime != preTime);
        }

        private void UpdateHScrollBar()
        {
            int timeCount = this.lastTime + 1 - this.dispTimeCount;

            this.hScrollBar.Maximum = (timeCount >= 0) ? timeCount : 0;
        }

        private void UpdatePaintParam()
        {
            int paintTimeUnitCount;

            //描画できるタイムの単位最大数
            paintTimeUnitCount = this.paintRect.Width / this.paintTimeUnitWidth;
            if (paintTimeUnitCount <= 0)
            {
                paintTimeUnitCount = 1;
            }

            //描画できるタイムの単位
            this.paintTimeUnit = this.dispTimeCount / paintTimeUnitCount;
            if ((this.paintTimeUnit % BASE_UNIT_TIME) > 0)
            {
                this.paintTimeUnit = ((this.paintTimeUnit / BASE_UNIT_TIME) + 1) * BASE_UNIT_TIME;
            }

            //描画時間の間隔
            this.paintTimeInterval = this.paintTimeUnit / BASE_UNIT_TIME;

            //描画するライン数
            this.paintLineCount = this.paintRect.Width / this.timeLineInterval + 1;

            //描画するタイムの最大
            this.paintTimeCount = this.paintLineCount * this.paintTimeInterval;
        }

        private bool GetCurLine(int paintStartTime, int paintEndTime, int actualTime, ref System.Drawing.Point p0, ref System.Drawing.Point p1)
        {
            if ((paintStartTime > actualTime) ||
                (paintEndTime <= actualTime))
            {
                return false;
            }

            int unitTime = actualTime / this.paintTimeInterval * this.paintTimeInterval;

            p0.X = this.paintRect.X + (unitTime - paintStartTime) / this.paintTimeInterval * this.timeLineInterval;
            p0.Y = this.Font.Height + 2;

            p1.X = p0.X;
            p1.Y = this.paintRect.Height;

            return true;
        }

        #endregion
    }
}
