// Copyright (C) 2008, 2010 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 306 2010-03-19 13:21:22Z panacoran $

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

namespace Protra.Lib.Lang
{
    /// <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
    {
        int rgb;
        double a1, a2, a3, a4;
        string text;
        int dh;
        DrawFunctionDelegate drawFunction;
        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;
            this.rgb = rgb;
            this.a1 = a1; this.a2 = a2; this.a3 = a3; this.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;
            this.rgb = rgb;
            this.text = text;
            a1 = x; a2 = y;
            this.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 int x;
        private int dx;
        private double minY;
        private double maxY;
        private List<DrawFunctionRecord> functionList;
        private Graphics g;
        private Font font;
        private Rectangle chartRect;

        /// <summary>
        /// コンストラクタ。
        /// </summary>
        public DrawBuiltins()
        {
            functionList = new List<DrawFunctionRecord>();
            minY = double.MaxValue;
            maxY = double.MinValue;
        }

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

        /// <summary>
        /// 横軸の間隔を設定する。
        /// </summary>
        public int Dx
        {
            set { dx = value; }
        }

         /// <summary>
        /// 縦軸の最小値を取得あるいは設定する。
        /// </summary>
        public double MinY
        {
            get { return minY; }
            set { minY = value; }
        }

        /// <summary>
        /// 縦軸の最大値を取得あるいは設定する。
        /// </summary>
        public double MaxY
        {
            get { return maxY; }
            set { maxY = value; }
        }

        /// <summary>
        /// 組み込み関数を実行する。
        /// </summary>
        /// <param name="name">名前</param>
        /// <param name="args">引数の配列</param>
        /// <param name="at">atパラメータ</param>
        /// <returns></returns>
        public override Value Invoke(string name, Value[] args, int at)
        {
            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);
                }
            }
            if (args.Length == 3 && name == "Rgb")
            {
                int r = (int)args[0].InnerValue;
                int g = (int)args[1].InnerValue;
                int b = (int)args[2].InnerValue;
                int 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);
                int i = 0;
                int rgb = Color.Black.ToArgb();
                if (args.Length == 5)
                    rgb = (int)args[i++].InnerValue;
                string text = (string)args[i++].InnerValue;
                double x = (double)args[i++].Cast(Value.Type.Float).InnerValue;
                double y = (double)args[i++].Cast(Value.Type.Float).InnerValue;
                int 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 = (double)args[1].Cast(Value.Type.Float).InnerValue;
                        a2 = (double)args[2].Cast(Value.Type.Float).InnerValue;
                        a3 = (double)args[3].Cast(Value.Type.Float).InnerValue;
                        a4 = (double)args[4].Cast(Value.Type.Float).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 = (double)args[1].Cast(Value.Type.Float).InnerValue;
                        a2 = (double)args[2].Cast(Value.Type.Float).InnerValue;
                        a3 = (double)args[3].Cast(Value.Type.Float).InnerValue;
                        a4 = (double)args[4].Cast(Value.Type.Float).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 = (double)args[1].Cast(Value.Type.Float).InnerValue;
                        a2 = (double)args[2].Cast(Value.Type.Float).InnerValue;
                        a3 = (double)args[3].Cast(Value.Type.Float).InnerValue;
                        a4 = (double)args[4].Cast(Value.Type.Float).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 = (double)args[1].Cast(Value.Type.Float).InnerValue;
                        a2 = (double)args[2].Cast(Value.Type.Float).InnerValue;
                        a3 = (double)args[3].Cast(Value.Type.Float).InnerValue;
                        a4 = (double)args[4].Cast(Value.Type.Float).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 = (double)args[1].Cast(Value.Type.Float).InnerValue;
                        a2 = (double)args[2].Cast(Value.Type.Float).InnerValue;
                        a3 = (double)args[3].Cast(Value.Type.Float).InnerValue;
                        a4 = (double)args[4].Cast(Value.Type.Float).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);
        }

        /// <summary>
        /// 描画関数を実行する。
        /// </summary>
        /// <param name="g">Graphicsのインスタンス</param>
        /// <param name="font">フォント</param>
        /// <param name="chartRect">描画領域を示すRectangle</param>
        public void Draw(Graphics g, Font font, Rectangle chartRect)
        {
            this.g = g;
            this.font = font;
            this.chartRect = chartRect;
            foreach (DrawFunctionRecord d in functionList)
                d.Execute();
        }

        private void DrawLine(int rgb, double x1, double y1, double x2, double y2)
        {
            AdjustY(ref y1);
            AdjustY(ref y2);
            g.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);
            g.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);
            g.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);
            this.g.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);
            this.g.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)
        {
            SizeF size = g.MeasureString(text, font);
            AdjustY(ref y);
            this.g.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;
        }
    }
}
