// Copyright (C) 2003 Daisuke Arai <darai@users.sourceforge.jp>
// Copyright (C) 2004 M. Ishiguro <mishiguro@users.sourceforge.jp>
// Copyright (C) 2004, 2007, 2008 panacoran <panacoran@users.sourceforge.jp>
// 
// This program is part of Protra.
//
// Protra 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/>.
// 
// $Id: ChartBox.cs 430 2011-10-19 06:31:33Z darai $

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Data;
using System.Windows.Forms;
using Protra.Lib.Data;
using Protra.Lib.Lang;

namespace Protra.Controls
{
	/// <summary>
	/// チャートを描画するコントロールです。
	/// </summary>
	public class ChartBox : UserControl
	{
		/// <summary>
		/// 左側にあける間隔
		/// </summary>
		private const int LeftMargin = 50;
        /// <summary>
        /// 右側にあける間隔
        /// </summary>
        private const int RightMargin = 10;
        /// <summary>
		/// 上側にあける間隔
		/// </summary>
		private const int TopMargin = 10;
		/// <summary>
		/// 下側にあける間隔
		/// </summary>
		private const int BottomMargin = 20;
        /// <summary>
        /// 横軸の間隔
        /// </summary>
        private const int Dx = 8;

		/// <summary>
		/// プログラムファイル名
		/// </summary>
        private string[] programFiles = new string[2];
		/// <summary>
		/// インタプリタ
		/// </summary>
        private Interpreter[] interpreters = new Interpreter[2];
		/// <summary>
		/// 描画対象となるPriceの配列
		/// </summary>
        private List<Price>[] priceLists = new List<Price>[2];
        /// <summary>
        /// チャートの右端のインデックス
        /// </summary>
        private int[] rightIndexes = new int[2];
        /// <summary>
        /// 表示モード
        /// </summary>
        private int chartMode;
        /// <summary>
        /// 日足と週足で異なるチャートを使用するか(使用するが1)
        /// </summary>
        private int useDifferentChart;

        /// <summary>
        /// 表示モードを設定する。
        /// </summary>
        public int ChartMode
        {
            set
            {
                chartMode = value;
                Invalidate();
            }
        }

        /// <summary>
        /// 日足と週足で異なるチャートを使用するかを設定する。
        /// </summary>
        public bool UseDifferentChart
        {
            set
            {
                if (value && useDifferentChart == 0)
                {
                    programFiles[1] = programFiles[0];
                    interpreters[1] = interpreters[0];
                }
                else if (!value && useDifferentChart == 1 && chartMode == 1)
                {
                    programFiles[0] = programFiles[1];
                    interpreters[0] = interpreters[1];
                }
                useDifferentChart = (value) ? 1 : 0;
            }
        }

		/// <summary>
		/// プログラムファイル名を設定する。
		/// </summary>
        /// <param name="mode">チャートモードを指定する。</param>
        /// <param name="file">プログラムファイル名を指定する。</param>
        private void SetProgramFile(int mode, string file)
        {
            programFiles[mode] = file;
            if (file == null)
            {
                interpreters[mode] = null;
                return;
            }
            try
            {
                interpreters[mode] = new Interpreter(file);
            }
            catch (ParseException exc)
            {
                interpreters[mode] = null;
                MessageBox.Show(exc.Message);
            }
        }

        /// <summary>
        /// 現在の表示モードに応じてプログラムファイルを設定する。
        /// </summary>
		public string ProgramFile
		{
			set
			{
                SetProgramFile(chartMode * useDifferentChart, value);
                Invalidate();
			}
		}

        /// <summary>
        /// 日足用のプログラムファイル名を取得あるいは設定する。
        /// </summary>
        public string DailyProgramFile
        {
            get
            {
                return programFiles[0];
            }
            set
            {
                SetProgramFile(0, value);
            }
        }

        /// <summary>
        /// 週足用のプログラムファイル名を取得あるいは設定する。
        /// </summary>
        public string WeeklyProgramFile
        {
            get
            {
                return programFiles[1];
            }
            set
            {
                SetProgramFile(1, value);
            }
        }

        /// <summary>
        /// 価格データを設定する。
        /// </summary>
        /// <param name="mode">チャートモード</param>
        /// <param name="prices">価格データ</param>
        public void SetPrices(int mode, List<Price> prices)
        {
            priceLists[mode] = prices;
            if (prices != null)
                rightIndexes[mode] = prices.Count - 1;
            if (mode == chartMode)
                Invalidate();
        }

        /// <summary>
        /// チャートの右端のインデックスを取得または設定する。
        /// </summary>
        public int RightIndex
        {
            get
            {
                return rightIndexes[chartMode];
            }
            set
            {
                rightIndexes[chartMode] = value;
                Invalidate();
            }
        }

		/// <summary>
		/// 何個分の価格が描画されるかを取得します。
		/// </summary>
		public int Count
		{
			get { return (this.Width - LeftMargin - RightMargin - 1) / Dx; } 
		}

        /// <summary>
        /// スクロールバーが必要かどうかを取得する。
        /// </summary>
        public bool NeedScrollBar
        {
            get
            {
                if (priceLists[chartMode] == null || priceLists[chartMode].Count < Count)
                    return false;
                return true;
            }
        }

        /// <summary>
        /// Double Bufferingを有効にする。
        /// </summary>
        /// <param name="e">イベントの引数</param>
        protected override void OnLoad(EventArgs e)
        {
            DoubleBuffered = true;
        }

        private Interpreter interpreter;
        private Rectangle chartRect;
        private List<Price> prices;
        private int rightIndex;
        private double maxY, minY;

        private Graphics graphics;
        private Brush brush;
        private Pen pen;
        private Pen dotPen;

        /// <summary>
        /// ペイントイベントを処理する。
        /// </summary>
        /// <param name="e">イベントの引数</param>
        protected override void  OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            interpreter = interpreters[chartMode * useDifferentChart];
            prices = priceLists[chartMode];
            if (prices == null || prices.Count == 0)
            {
                e.Graphics.DrawString("銘柄を選択してください。",
                    Font, new SolidBrush(ForeColor), TopMargin, LeftMargin);
                return;
            }
            if (interpreter == null)
            {
                e.Graphics.DrawString("右クリックしてチャートを指定してください。",
                    Font, new SolidBrush(ForeColor), TopMargin, LeftMargin);
                return;
            }
            chartRect = new Rectangle(
                LeftMargin, TopMargin,
                Width - LeftMargin - RightMargin - 1,
                Height - TopMargin - BottomMargin);
            rightIndex = rightIndexes[chartMode];

            try
            {
                VirtualRendereing();
            }
            catch (Exception exc)
            {
                interpreters[chartMode * useDifferentChart] = null;
                MessageBox.Show(exc.Message);
                return;
            }

            graphics = e.Graphics;
            SetGraphicsParameters();
            DrawVirticalAxis();
            DrawHorizontalAxis();

            // 本番の描画
            Region oldClip = graphics.Clip;
            graphics.Clip = new Region(chartRect);
            ((DrawBuiltins)interpreter.Builtins).Draw(graphics, Font, chartRect);
            graphics.Clip = oldClip;

            // 枠の描画
            graphics.DrawRectangle(pen, chartRect);

            DrawIndicatorNames();
            DrawIndicatorValue(RightIndex);
        }

        /// <summary>
        /// 仮想描画によって縦軸のスケールを得る
        /// </summary>
        private void VirtualRendereing()
        {
            // グローバル変数の設定
			interpreter.GlobalVariableTable.Clear();
			interpreter.GlobalVariableTable["$Names"] = new Value(new Value[6]);
			interpreter.GlobalVariableTable["$Colors"] = new Value(new Value[6]);
			interpreter.GlobalVariableTable["$IsTimeSeries"] = new Value(true);

            // 組み込み関数の設定
            DrawBuiltins blt = new DrawBuiltins();
            int count = chartRect.Width / Dx;
            blt.Index = Math.Max(0, RightIndex - Count + 1);
            blt.Mode = chartMode;
            blt.Prices = prices;
            blt.X = chartRect.Right - (rightIndex - blt.Index + 1) * Dx;
            blt.Dx = Dx;
            blt.RightIndex = rightIndex;
            interpreter.Builtins = blt;

            while (blt.Index <= rightIndex)
            {
                interpreter.Execute();
                blt.X += Dx;
                blt.Index++;
            }
        }

        /// <summary>
        /// 描画に必要なデータを設定する。
        /// </summary>
        void SetGraphicsParameters()
        {
			brush = new SolidBrush(ForeColor);
			pen = new Pen(ForeColor);
			Color dotColor = Color.FromArgb(
		    (BackColor.A + ForeColor.A) / 2,
			(BackColor.R + ForeColor.R) / 2,
			(BackColor.G + ForeColor.G) / 2,
			(BackColor.B + ForeColor.B) / 2);
			dotPen = new Pen(dotColor);
			dotPen.DashStyle = DashStyle.Dot;
        }

        /// <summary>
        /// 縦軸を描画する。
        /// </summary>
        private void DrawVirticalAxis()
        {
            var blt = (DrawBuiltins)interpreter.Builtins;

            if (blt.MaxY == double.MinValue)
                return;

            var interval = CalcInterval();
            maxY = blt.MaxY;
            minY = blt.MinY;
            blt.MaxY = Math.Ceiling(blt.MaxY / interval) * interval;
            blt.MinY = Math.Floor(blt.MinY / interval) * interval;
            double m = blt.MinY + interval;
            double ratio = chartRect.Height / (blt.MaxY - blt.MinY);
            while (m < blt.MaxY)
            {
                m = Math.Round(m, 2);
                string text = m.ToString();

                SizeF size = graphics.MeasureString(text, Font);
                graphics.DrawString(text, Font, brush,
                    chartRect.Left - size.Width,
                    (float)(chartRect.Bottom - (m - blt.MinY) * ratio - size.Height / 2));
                graphics.DrawLine(dotPen,
                    chartRect.Left, (float)(chartRect.Bottom - (m - blt.MinY) * ratio),
                    chartRect.Right, (float)(chartRect.Bottom - (m - blt.MinY) * ratio));
                m += interval;
            }
        }

        /// <summary>
        /// 縦軸の間隔を計算する。
        /// </summary>
        /// <returns>間隔</returns>
        private double CalcInterval()
        {
            var blt = (DrawBuiltins)interpreter.Builtins;

            if (blt.MaxY == blt.MinY)
            {
                blt.MaxY += 1;
                blt.MinY -= 1;
            }
            double interval = (blt.MaxY - blt.MinY) / 5;
            double a = 0.001;
            while (true)
            {
                if (interval < 10 * a)
                {
                    for (double b = a; b <= 10 * a; b += a)
                        if (b >= interval)
                        {
                            interval = b;
                            break;
                        }
                    break;
                }
                a *= 10;
            }
            return interval;
        }

        /// <summary>
        /// 日付を描画する。
        /// </summary>
        private void DrawHorizontalAxis()
        {
            int x;

            if (interpreter.GlobalVariableTable["$IsTimeSeries"].IsTrue)
            {
                int count = chartRect.Width / Dx;
                int index = Math.Max(0, rightIndex - count + 1);
                x = chartRect.Right - (rightIndex - index + 1) * Dx;
                var preDate = prices[index].Date;
                while (index <= rightIndex)
                {
                    DateTime date = prices[index].Date;
                    int monthInterval = (chartMode == 0) ? 1 : 3;
                    if ((date.Month - 1) % monthInterval == 0)
                        if (date.Month != preDate.Month)
                        {
                            string text = date.ToString("yy/MM");
                            SizeF size = graphics.MeasureString(text, this.Font);
                            graphics.DrawString(text, Font, brush,
                                x - (Dx + size.Width) / 2, chartRect.Bottom);
                            graphics.DrawLine(dotPen,
                                x - Dx / 2, chartRect.Top,
                                x - Dx / 2, chartRect.Bottom);
                            preDate = date;
                        }
                    x += Dx;
                    index++;
                }
            }
        }

        /// <summary>
        /// 指標のラベルを描画する。
        /// </summary>
        private void DrawIndicatorNames()
        {
            var names = (Value[])interpreter.GlobalVariableTable["$Names"].InnerValue;
            var colors = (Value[])interpreter.GlobalVariableTable["$Colors"].InnerValue;
            float y = TopMargin + 1;
            for (int i = 0; i < names.Length; i++)
            {
                if (names[i] == null || colors[i] == null)
                    continue;
                SizeF size = graphics.MeasureString((string)names[i].InnerValue, Font);
                graphics.FillRectangle(new SolidBrush(BackColor), LeftMargin + 1, y, size.Width, size.Height);
                Brush color = new SolidBrush(Color.FromArgb((int)colors[i].InnerValue));
                graphics.DrawString((string)names[i].InnerValue, Font, color, LeftMargin + 1, y);
                y += size.Height * 2;
            }
        }

        /// <summary>
        /// マウスポインタのX座標から価格データのインデックスを計算する。
        /// </summary>
        /// <param name="x">マウスポインタのX座標</param>
        /// <returns></returns>
        public int CalcIndexFromX(int x)
        {
            if (x <= LeftMargin || x >= Width - RightMargin)
                return -1;
            return RightIndex - (Width - RightMargin - x - Dx / 2 - 1) / Dx;
        }

        /// <summary>
        /// 指標の値を描画する。
        /// </summary>
        /// <param name="index"></param>
        public void DrawIndicatorValue(int index)
        {
            if (interpreter == null || interpreter.GlobalVariableTable.Count == 0)
                return;
            var names = (Value[])interpreter.GlobalVariableTable["$Names"].InnerValue;
            var colors = (Value[])interpreter.GlobalVariableTable["$Colors"].InnerValue;
            bool dispose = false;
            if (graphics == null)
            {
                graphics = CreateGraphics();
                dispose = true;
            }
            SizeF size = graphics.MeasureString("1000000", Font);
            Brush background = new SolidBrush(BackColor);
            float y = TopMargin + 1 + size.Height;
            int i = 0;
            for (; i < names.Length; i++, y += size.Height * 2)
            {
                if (names[i] == null || colors[i] == null)
                    break;
                Brush color = new SolidBrush(Color.FromArgb((int)colors[i].InnerValue));
                string str;
                try
                {
                    var blt = (DrawBuiltins)interpreter.Builtins;
                    double v = blt.Indicators[i][index];
                    if (maxY <= 10 && minY >= -10)
                        str = v.ToString("0.00");
                    else if (maxY <= 100 && minY >= -100)
                        str = v.ToString("0.0");
                    else
                        str = v.ToString("0");
                }
                catch (KeyNotFoundException)
                {
                    continue;
                }
                graphics.FillRectangle(background, LeftMargin + 1, y, size.Width, size.Height);
                SizeF s = graphics.MeasureString(str, Font);
                graphics.DrawString(str, Font, color, LeftMargin + 1 + size.Width - s.Width, y);
            }
            if (dispose)
                graphics.Dispose();
            graphics = null;
        }

        /// <summary>
        /// SizeChangedイベントを処理する。
        /// </summary>
        /// <param name="e">イベントの引数</param>
        protected override void  OnSizeChanged(EventArgs e)
        {
 	        base.OnSizeChanged(e);
            Invalidate();
        }
	}
}