// Copyright (C) 2003 Daisuke Arai <darai@users.sourceforge.jp>
// Copyright (C) 2004 M. Ishiguro <mishiguro@users.sourceforge.jp>
// Copyright (C) 2004, 2007, 2008, 2013 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 452 2013-05-26 06:48:26Z panacoran $

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

namespace Protra.Controls
{
    /// <summary>
    /// チャートを描画するコントロールです。
    /// </summary>
    public class ChartBox : UserControl
    {
        private const int LeftMargin = 50;
        private const int RightMargin = 10;
        private const int TopMargin = 10;
        private const int BottomMargin = 20;
        private const int Dx = 8; // 横軸の間隔
        private readonly Dictionary<TimeFrame, string> _programs = new Dictionary<TimeFrame, string>();
        private readonly Dictionary<TimeFrame, Interpreter> _interpreters = new Dictionary<TimeFrame, Interpreter>();
        private bool _useDifferentChart;
        private Graphics _graphics;
        private readonly Color _dotColor = Color.FromArgb(0x80, Color.Gray);
        private Rectangle _chartRect;
        private double _maxY, _minY;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public ChartBox()
        {
            _programs[TimeFrame.Daily] = _programs[TimeFrame.Weekly] = null;
            _interpreters[TimeFrame.Daily] = _interpreters[TimeFrame.Weekly] = null;
        }

        private string Code
        {
            get { return ((ChartPanel)Parent).Code; }
        }

        private TimeFrame TimeFrame
        {
            get { return ((ChartPanel)Parent).TimeFrame; }
        }

        private PriceList Prices
        {
            get { return ((ChartPanel)Parent).Prices; }
        }

        private int RightIndex
        {
            get { return ((ChartPanel)Parent).RightIndex; }
        }

        /// <summary>
        /// 日足と週足で異なるチャートを使用するかを設定する。
        /// </summary>
        public bool UseDifferentChart
        {
            set
            {
                if (value && !_useDifferentChart)
                {
                    // 週足のプログラムが設定されていない場合があるので日足の設定を移す。
                    _programs[TimeFrame.Weekly] = _programs[TimeFrame.Daily];
                    _interpreters[TimeFrame.Weekly] = _interpreters[(int)TimeFrame.Daily];
                }
                else if (!value && _useDifferentChart && TimeFrame == TimeFrame.Weekly)
                {
                    // 週足で使用していたプログラムを日足に移す。
                    _programs[TimeFrame.Daily] = _programs[TimeFrame.Weekly];
                    _interpreters[TimeFrame.Daily] = _interpreters[TimeFrame.Weekly];
                }
                _useDifferentChart = value;
            }
        }

        /// <summary>
        /// 現在のタイムフレームのプログラムを取得する。
        /// </summary>
        public string Program
        {
            set
            {
                SetProgram(ProgramTimeFrame, value);
                Invalidate();
            }
        }

        /// <summary>
        /// 現在のタイムフレームのプログラムを取得または設定する。
        /// </summary>
        private Interpreter Interpreter
        {
            get { return _interpreters[ProgramTimeFrame]; }
            set { _interpreters[ProgramTimeFrame] = value; }
        }

        private TimeFrame ProgramTimeFrame
        {
            get { return _useDifferentChart ? TimeFrame : TimeFrame.Daily; }
        }

        /// <summary>
        /// 日足用のプログラムを取得または設定する。
        /// </summary>
        public string DailyProgram
        {
            get { return _programs[TimeFrame.Daily]; }
            set { SetProgram(TimeFrame.Daily, value); }
        }

        /// <summary>
        /// 週足用のプログラムを取得または設定する。
        /// </summary>
        public string WeeklyProgram
        {
            get { return _programs[TimeFrame.Weekly]; }
            set { SetProgram(TimeFrame.Weekly, value); }
        }

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

        /// <summary>
        /// ChartPanelに占める高さの比率を取得または設定する。
        /// </summary>
        public double Proportion { get; set; }

        /// <summary>
        /// プログラムを設定する。
        /// </summary>
        /// <param name="timeFrame">タイムフレーム</param>
        /// <param name="file">プログラム</param>
        private void SetProgram(TimeFrame timeFrame, string file)
        {
            _programs[timeFrame] = file;
            if (file == null)
            {
                _interpreters[timeFrame] = null;
                return;
            }
            try
            {
                _interpreters[timeFrame] = new Interpreter(file);
            }
            catch (ParseException exc)
            {
                _interpreters[timeFrame] = null;
                MessageBox.Show(exc.Message);
            }
        }

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

        /// <summary>
        /// ペイントイベントを処理する。
        /// </summary>
        /// <param name="e">イベントの引数</param>
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            if (Code == null)
            {
                e.Graphics.DrawString("銘柄を選択してください。",
                                      Font, new SolidBrush(ForeColor), TopMargin, LeftMargin);
                return;
            }
            if (Prices == null)
            {
                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);
            try
            {
                VirtualRendereing();
            }
            catch (Exception exc)
            {
                Interpreter = null;
                MessageBox.Show(exc.Message);
                return;
            }

            _graphics = e.Graphics;
            DrawVirticalAxis();
            DrawHorizontalAxis();

            // 本番の描画
            var oldClip = _graphics.Clip;
            _graphics.Clip = new Region(_chartRect);
            ((DrawBuiltins)Interpreter.Builtins).Draw(_graphics, Font, _chartRect);
            _graphics.Clip = oldClip;

            // 枠の描画
            _graphics.DrawRectangle(new Pen(ForeColor), _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);

            // 組み込み関数の設定
            var index = Math.Max(0, RightIndex - Count + 1);
            var blt = new DrawBuiltins
                {
                    Prices = Prices,
                    Index = index,
                    RightIndex = RightIndex,
                    X = _chartRect.Right - (RightIndex - index + 1) * Dx,
                    Dx = Dx
                };
            Interpreter.Builtins = blt;

            while (blt.Index <= RightIndex)
            {
                Interpreter.Execute();
                blt.X += Dx;
                blt.Index++;
            }
        }

        /// <summary>
        /// 縦軸を描画する。
        /// </summary>
        private void DrawVirticalAxis()
        {
            var blt = (DrawBuiltins)Interpreter.Builtins;
// ReSharper disable CompareOfFloatsByEqualityOperator
            if (blt.MaxY == double.MinValue)
// ReSharper restore CompareOfFloatsByEqualityOperator
                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;
            var m = blt.MinY + interval;
            var ratio = _chartRect.Height / (blt.MaxY - blt.MinY);
            while (m < blt.MaxY)
            {
                m = Math.Round(m, 2);
                var text = m.ToString();

                var size = _graphics.MeasureString(text, Font);
                _graphics.DrawString(text, Font, new SolidBrush(ForeColor),
                                     _chartRect.Left - size.Width,
                                     (float)(_chartRect.Bottom - (m - blt.MinY) * ratio - size.Height / 2));
                _graphics.DrawLine(new Pen(_dotColor),
                                   _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;
// ReSharper disable CompareOfFloatsByEqualityOperator
            if (blt.MaxY == blt.MinY)
// ReSharper restore CompareOfFloatsByEqualityOperator
            {
                blt.MaxY += 1;
                blt.MinY -= 1;
            }
            var interval = (blt.MaxY - blt.MinY) / 5;
            var a = 0.001;
            while (true)
            {
                if (interval < 10 * a)
                {
                    for (var b = a; b <= 10 * a; b += a)
                        if (b >= interval)
                        {
                            interval = b;
                            break;
                        }
                    break;
                }
                a *= 10;
            }
            return interval;
        }

        /// <summary>
        /// 日付を描画する。
        /// </summary>
        private void DrawHorizontalAxis()
        {
            if (Interpreter.GlobalVariableTable["$IsTimeSeries"].IsTrue)
            {
                var index = Math.Max(0, RightIndex - Count + 1);
                var x = _chartRect.Right - (RightIndex - index + 1) * Dx;
                var preDate = Prices[index].Date;
                while (index <= RightIndex)
                {
                    var date = Prices[index].Date;
                    var monthInterval = (TimeFrame == TimeFrame.Daily) ? 1 : 3;
                    if ((date.Month - 1) % monthInterval == 0)
                        if (date.Month != preDate.Month)
                        {
                            var text = date.ToString("yy/MM");
                            var size = _graphics.MeasureString(text, Font);
                            _graphics.DrawString(text, Font, new SolidBrush(ForeColor),
                                                 x - (Dx + size.Width) / 2, _chartRect.Bottom);
                            _graphics.DrawLine(new Pen(_dotColor),
                                               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;
            var y = TopMargin + 1f;
            for (var i = 0; i < names.Length; i++)
            {
                if (names[i] == null || colors[i] == null)
                    continue;
                var size = _graphics.MeasureString((string)names[i].InnerValue, Font);
                _graphics.FillRectangle(new SolidBrush(BackColor), LeftMargin + 1, y, size.Width, size.Height);
                var 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 (Interpreter == null || 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;
            var dispose = false;
            if (_graphics == null)
            {
                _graphics = CreateGraphics();
                dispose = true;
            }
            var size = _graphics.MeasureString("1000000", Font);
            var background = new SolidBrush(BackColor);
            var y = TopMargin + 1 + size.Height;
            var i = 0;
            for (; i < names.Length; i++, y += size.Height * 2)
            {
                if (names[i] == null || colors[i] == null)
                    break;
                string str;
                try
                {
                    var blt = (DrawBuiltins)Interpreter.Builtins;
                    var 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);
                var s = _graphics.MeasureString(str, Font);
                var color = new SolidBrush(Color.FromArgb((int)colors[i].InnerValue));
                _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();
        }
    }
}