// Copyright (C) 2008, 2010, 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: DrawBuiltins.cs 458 2013-06-14 02:19:13Z panacoran $

using System;
using System.Collections.Generic;
using System.Drawing;

namespace Protra.Lib.Lang.Builtins
{
    /// <summary>
    /// 描画系の組み込み関数のデリゲート。
    /// </summary>
    /// <param name="rgb">色</param>
    /// <param name="a1">座標1</param>
    /// <param name="a2">座標2</param>
    /// <param name="a3">座標3</param>
    /// <param name="a4">座標4</param>
    public delegate void DrawFunctionDelegate(int rgb, double a1, double a2, double a3, double a4);

    /// <summary>
    /// 組み込み関数DrawStringのデリゲート。
    /// </summary>
    /// <param name="rgb">色</param>
    /// <param name="text">文字列</param>
    /// <param name="x">x座標</param>
    /// <param name="y">y座標</param>
    /// <param name="dh">オフセット</param>
    public delegate void DrawStringDelegate(int rgb, string text, double x, double y, int dh);

    /// <summary>
    /// 描画系の組み込み関数の実行を記録する構造体。
    /// </summary>
    public struct DrawFunctionRecord
    {
        private readonly int _rgb;
        private readonly double _a1, _a2, _a3, _a4;
        private readonly string _text;
        private readonly int _dh;
        private readonly DrawFunctionDelegate _drawFunction;
        private readonly DrawStringDelegate _drawString;

        /// <summary>
        /// コンストラクタ。
        /// </summary>
        /// <param name="f">実行する関数のデリゲート</param>
        /// <param name="rgb">色</param>
        /// <param name="a1">座標1</param>
        /// <param name="a2">座標2</param>
        /// <param name="a3">座標3</param>
        /// <param name="a4">座標4</param>
        public DrawFunctionRecord(DrawFunctionDelegate f, int rgb, double a1, double a2, double a3, double a4)
        {
            _drawFunction = f;
            _rgb = rgb;
            _a1 = a1;
            _a2 = a2;
            _a3 = a3;
            _a4 = a4;
            _drawString = null;
            _text = null;
            _dh = 0;
        }

        /// <summary>
        /// コンストラクタ。
        /// </summary>
        /// <param name="f">DrawStringのデリゲート</param>
        /// <param name="rgb">色</param>
        /// <param name="text">文字列</param>
        /// <param name="x">x座標</param>
        /// <param name="y">y座標</param>
        /// <param name="dh">オフセット</param>
        public DrawFunctionRecord(DrawStringDelegate f, int rgb, string text, double x, double y, int dh)
        {
            _drawString = f;
            _rgb = rgb;
            _text = text;
            _a1 = x;
            _a2 = y;
            _dh = dh;
            _drawFunction = null;
            _a3 = 0;
            _a4 = 0;
        }

        /// <summary>
        /// 記録された内容を実際に実行する。
        /// </summary>
        public void Execute()
        {
            if (_drawString != null)
            {
                _drawString(_rgb, _text, _a1, _a2, _dh);
                return;
            }
            _drawFunction(_rgb, _a1, _a2, _a3, _a4);
        }
    }

    /// <summary>
    /// 描画系の組み込み関数を処理するクラス。
    /// </summary>
    public class DrawBuiltins : BasicBuiltins
    {
        private readonly List<DrawFunctionRecord> _functionList;
        private Graphics _graphics;
        private Font _font;
        private Rectangle _chartRect;

        /// <summary>
        /// X座標を取得または設定する。
        /// </summary>
        public int X { get; set; }

        /// <summary>
        /// 横軸の間隔を取得または設定する。
        /// </summary>
        public int Dx { get; set; }

        /// <summary>
        /// 縦軸の最小値を取得または設定する。
        /// </summary>
        public double MinY { get; set; }

        /// <summary>
        /// 縦軸の最大値を取得または設定する。
        /// </summary>
        public double MaxY { get; set; }

        /// <summary>
        /// 指標の値を持つ辞書の配列を取得または設定する。
        /// </summary>
        public Dictionary<int, double>[] Indicators { get; set; }

        /// <summary>
        /// コンストラクタ。
        /// </summary>
        public DrawBuiltins()
        {
            _functionList = new List<DrawFunctionRecord>();
            MinY = double.MaxValue;
            MaxY = double.MinValue;
            Indicators = new Dictionary<int, double>[6];
            for (var i = 0; i < 6; i++)
                Indicators[i] = new Dictionary<int, double>();
        }

        /// <summary>
        /// 組み込み関数を実行する。
        /// </summary>
        /// <param name="name">名前</param>
        /// <param name="args">引数の配列</param>
        /// <param name="at">int型@作用素</param>
        /// <param name="ats">string型@作用素</param>
        /// <returns></returns>
        public override Value Invoke(string name, Value[] args, int at, string ats)
        {
            if (args.Length == 0)
            {
                switch (name)
                {
                    case "X":
                        return new Value(X + at * Dx);
                    case "Dx":
                        return new Value(Dx);
                    default:
                        return base.Invoke(name, args, at, ats);
                }
            }
            if (args.Length == 2 && name == "Indicator")
            {
                // 必要なデータがそろっていないときにTIlibがnullを返すので、
                // nullをエラーにしないようにしておく。
                if (args[1] == null)
                    return null;
                var n = (int)args[0].InnerValue;
                var v = Convert.ToDouble(args[1].InnerValue);
                Indicators[n][Index + at] = v;
                return null;
            }
            if (args.Length == 3 && name == "Rgb")
            {
                var r = (int)args[0].InnerValue;
                var g = (int)args[1].InnerValue;
                var b = (int)args[2].InnerValue;
                var rgb = Color.FromArgb(r, g, b).ToArgb();
                return new Value(rgb);
            }
            if (name == "DrawString")
            {
                if (args.Length != 4 && args.Length != 5)
                    return base.Invoke(name, args, at, ats);
                var i = 0;
                var rgb = Color.Black.ToArgb();
                if (args.Length == 5)
                    rgb = (int)args[i++].InnerValue;
                var text = (string)args[i++].InnerValue;
                var x = Convert.ToDouble(args[i++].InnerValue);
                var y = Convert.ToDouble(args[i++].InnerValue);
                var dh = (int)args[i].InnerValue;
                MaxY = Math.Max(y, MaxY);
                MinY = Math.Min(y, MinY);
                _functionList.Add(new DrawFunctionRecord(DrawString, rgb, text, x, y, dh));
                return null;
            }
            if (args.Length == 5)
            {
                int rgb;
                double a1, a2, a3, a4;
                switch (name)
                {
                    case "DrawLine":
                        rgb = (int)args[0].InnerValue;
                        a1 = Convert.ToDouble(args[1].InnerValue);
                        a2 = Convert.ToDouble(args[2].InnerValue);
                        a3 = Convert.ToDouble(args[3].InnerValue);
                        a4 = Convert.ToDouble(args[4].InnerValue);
                        MaxY = Math.Max(Math.Max(a2, a4), MaxY);
                        MinY = Math.Min(Math.Min(a2, a4), MinY);
                        _functionList.Add(new DrawFunctionRecord(DrawLine, rgb, a1, a2, a3, a4));
                        return null;
                    case "DrawRectangle":
                        rgb = (int)args[0].InnerValue;
                        a1 = Convert.ToDouble(args[1].InnerValue);
                        a2 = Convert.ToDouble(args[2].InnerValue);
                        a3 = Convert.ToDouble(args[3].InnerValue);
                        a4 = Convert.ToDouble(args[4].InnerValue);
                        MaxY = Math.Max(Math.Max(a2, a2 + a4), MaxY);
                        MinY = Math.Min(Math.Min(a2, a2 + a4), MinY);
                        _functionList.Add(new DrawFunctionRecord(DrawRectangle, rgb, a1, a2, a3, a4));
                        return null;
                    case "DrawEllipse":
                        rgb = (int)args[0].InnerValue;
                        a1 = Convert.ToDouble(args[1].InnerValue);
                        a2 = Convert.ToDouble(args[2].InnerValue);
                        a3 = Convert.ToDouble(args[3].InnerValue);
                        a4 = Convert.ToDouble(args[4].InnerValue);
                        MaxY = Math.Max(Math.Max(a2, a2 + a4), MaxY);
                        MinY = Math.Min(Math.Min(a2, a2 + a4), MinY);
                        _functionList.Add(new DrawFunctionRecord(DrawEllipse, rgb, a1, a2, a3, a4));
                        return null;
                    case "FillRectangle":
                        rgb = (int)args[0].InnerValue;
                        a1 = Convert.ToDouble(args[1].InnerValue);
                        a2 = Convert.ToDouble(args[2].InnerValue);
                        a3 = Convert.ToDouble(args[3].InnerValue);
                        a4 = Convert.ToDouble(args[4].InnerValue);
                        MaxY = Math.Max(Math.Max(a2, a2 + a4), MaxY);
                        MinY = Math.Min(Math.Min(a2, a2 + a4), MinY);
                        _functionList.Add(new DrawFunctionRecord(FillRectangle, rgb, a1, a2, a3, a4));
                        return null;
                    case "FillEllipse":
                        rgb = (int)args[0].InnerValue;
                        a1 = Convert.ToDouble(args[1].InnerValue);
                        a2 = Convert.ToDouble(args[2].InnerValue);
                        a3 = Convert.ToDouble(args[3].InnerValue);
                        a4 = Convert.ToDouble(args[4].InnerValue);
                        MaxY = Math.Max(Math.Max(a2, a2 + a4), MaxY);
                        MinY = Math.Min(Math.Min(a2, a2 + a4), MinY);
                        _functionList.Add(new DrawFunctionRecord(FillEllipse, rgb, a1, a2, a3, a4));
                        return null;
                }
            }
            return base.Invoke(name, args, at, ats);
        }

        /// <summary>
        /// 描画関数を実行する。
        /// </summary>
        /// <param name="g">Graphicsのインスタンス</param>
        /// <param name="f">フォント</param>
        /// <param name="rect">描画領域を示すRectangle</param>
        public void Draw(Graphics g, Font f, Rectangle rect)
        {
            _graphics = g;
            _font = f;
            _chartRect = rect;
            foreach (var d in _functionList)
                d.Execute();
        }

        private void DrawLine(int rgb, double x1, double y1, double x2, double y2)
        {
            AdjustY(ref y1);
            AdjustY(ref y2);
            _graphics.DrawLine(new Pen(Color.FromArgb(rgb)),
                               (float)x1, (float)y1, (float)x2, (float)y2);
        }

        private void DrawRectangle(int rgb, double x, double y, double width, double height)
        {
            AdjustY(ref y, ref height);
            _graphics.DrawRectangle(new Pen(Color.FromArgb(rgb)),
                                    (float)x, (float)y, (float)width, (float)height);
        }

        private void DrawEllipse(int rgb, double x, double y, double width, double height)
        {
            AdjustY(ref y, ref height);
            _graphics.DrawEllipse(new Pen(Color.FromArgb(rgb)),
                                  (float)x, (float)y, (float)width, (float)height);
        }

        private void FillRectangle(int rgb, double x, double y, double width, double height)
        {
            AdjustY(ref y, ref height);
            _graphics.FillRectangle(new SolidBrush(Color.FromArgb(rgb)),
                                    (float)x, (float)y, (float)width, (float)height);
        }

        private void FillEllipse(int rgb, double x, double y, double width, double height)
        {
            AdjustY(ref y, ref height);
            _graphics.FillEllipse(new SolidBrush(Color.FromArgb(rgb)),
                                  (float)x, (float)y, (float)width, (float)height);
        }

        private void DrawString(int rgb, string text, double x, double y, int dh)
        {
            var size = _graphics.MeasureString(text, _font);
            AdjustY(ref y);
            _graphics.DrawString(text, _font, new SolidBrush(Color.FromArgb(rgb)),
                                 (float)(x - size.Width / 2), (float)(y - size.Height / 2) - dh);
        }

        private void AdjustY(ref double y)
        {
            y = _chartRect.Bottom - _chartRect.Height * (y - MinY) / (MaxY - MinY);
        }

        private void AdjustY(ref double y, ref double height)
        {
            AdjustY(ref y);
            height = _chartRect.Height * height / (MaxY - MinY);
            // 描画クラスのy座標がProtraと反対で下向きなので、
            // 高さが正の場合は始点を上にずらして描画し、
            // 負の場合は始点はそのままで高さを反転して描画する。
            if (height > 0)
                y -= height;
            else
                height *= -1;
            // 1より小さいとまったく描画されなくなるので1に切り上げる。
            if (height < 1)
                height = 1;
        }
    }
}