//------------------------------------------------------------------------------
//	ログ･スケールのグラフを描くためのクラス
//		元のクラス：LogScale「作成者：三上直樹，2022/08/29 作成，©三上直樹 2022」
//
//		LogScaleX	横軸のみログスケールに対応するクラス
//		LogScaleXY	縦軸，横軸がログスケールに対応するクラス
//
//		2023/01/15	横軸を 4 kHz ～ 10 kHz で描画する際に間の縦線を描画しない
//					不具合を修正
//
//		2023/04/03	ToFrq() メソッドを追加
//
//		作成者：三上直樹，2023/04/03 作成，©三上直樹 2023
//------------------------------------------------------------------------------

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics;

using static System.Math;

namespace Mikami.GraphicsLib
{
	//-------------------------------------------------------------------
	// 横軸がログスケールのグラフを描画するためのクラス
	//-------------------------------------------------------------------
	public class LogScaleX
	{
		private const string ASSERT_MSG = "0 以下です．";	// Debug.Assert() のメッセージ

		private readonly Values x_;				// x 軸用
		protected readonly Graphics gr_;		// グラフィックス用

		protected Func<double, float> ToFloat = x => (float)x;	// DrawGraph() で使用

		public float Width{ get; private set; }	// グラフの幅

		// 描画に必要な定数を格納するための構造体
		public readonly struct Values
		{
			public readonly float min, max, dec;
			public Values(double uMin, double uMax, double uDec)
			{
				Debug.Assert(uMin > 0, ASSERT_MSG);
				min = (float)uMin;	// 座標軸の目盛の最小値
				Debug.Assert(uMax > 0, ASSERT_MSG);
				max = (float)uMax;	// 座標軸の目盛の最大値
				dec = (float)uDec;	// 10 倍に対応する長さ
			}
		}

		// コンストラクタ，縦軸がリニアスケールの場合
		//		u	横軸に対する Values 構造体
		public LogScaleX(Graphics gr, Values u)
		{
			gr_ = gr;
			x_ = u;
			Width = X(x_.max) - X(x_.min);
		}

		// x 座標値の取得
		public float X(double x)
		{
			Debug.Assert(x > 0, ASSERT_MSG);
			return x_.dec*(float)Log10(x/x_.min);
		}

		// x 座標値から周波数への変換
		public float ToFrq(double x) => x_.min*(float)Pow(10, x/x_.dec);

		// x 軸の数値に対する目盛線の描画
		//		h1	目盛線の長さ
		//		h2	10 のべき乗に対する目盛線の長さを h1 と異なる値に
		//			この引数を設定する．設定しない場合はすべて h1 と同じ
		public void DrawAxisX(Pen aPen, float h1, float h2 = 0)
		{
			if (h2 == 0) h2 = h1;
			DrawScale(x_, x => gr_.DrawLine(aPen, X(x), 0, X(x),
											-(IsPow10(x) ? h1 : h2)));
		}

		// DrawAxisX()，DrawAxisY() で使う
		protected void DrawScale(Values value, Action<float> DrawLine)
		{
			int logMin = (int)Floor(Log10(value.min) + 0.001);
			int loop = (int)Floor(Log10(value.max) + 0.001) - logMin;

			DrawLine(value.min);	// 最小値に対応する線
			// 座標値の増分
			float du = (float)Pow(10.0, logMin);
			// 最小値に対応する線の次の座標値
			float u1 = (float)(Round(value.min/du) + 1)*du;
			
			for (int n=0; n<=loop; n++)
			{
				float uMax = Min(10*du, value.max);
				for (float u=u1; u<uMax*0.99f; u+=du) DrawLine(u);
					
				du = uMax;			// 値の増分を 10 倍する
				u1 = du;			// 次の for ループの最初の値
			}
			DrawLine(value.max);	// 最大値に対応する線
		}

		// y 軸の数値に対するリニアな目盛線の描画
		//		dy	目盛線の間隔
		//		num	目盛線の数
		public void DrawAxisYLinear(Pen aPen, float dy, int num, float w0 = 0)
		{
			if (w0 == 0) w0 = Width;
			for (int n=0; n<num; n++) gr_.DrawLine(aPen, 0, -n*dy, w0, -n*dy);
		}

		// グラフの描画，Graphics.DrawLines() メソッドを使用
		//		描画領域を限定する場合は，y0 と size を 0 以外の値に設定する
		//		y0		描画領域に対応する長方形の上端の y 座標値
		//		size	描画領域に対応する長方形の高さ
		public void DrawLines(Func<double, double> y, int nPoints, Pen aPen,
							  float y0 = 0, float size = 0,
							  SmoothingMode mode = SmoothingMode.AntiAlias)
			=> DrawGraph(y, nPoints, aPen, gr_.DrawLines, y0, size, mode);

		// グラフの描画，Graphics.DrawCurve() メソッドを使用
		//		描画領域を限定する場合の設定は DrawLines() と同じ
		public void DrawCurve(Func<double, double> y, int nPoints, Pen aPen,
							  float y0 = 0, float size = 0,
							  SmoothingMode mode = SmoothingMode.AntiAlias)
			=> DrawGraph(y, nPoints, aPen, gr_.DrawCurve, y0, size, mode);

		// DrawLines(), DrawCurve() で使用
		private void DrawGraph(Func<double, double> y, int nPoints, Pen aPen,
							   Action<Pen, PointF[]> Draw, 
							   float y0, float size, SmoothingMode mode)
		{
			// 隣同士の描画点に対応する x 軸方向の値の比
			double ratio = (float)Pow(x_.max/x_.min, 1.0/(nPoints - 1));

			double xVal = x_.min;
			PointF[] p = new PointF[nPoints+1];
			for (int n=0; n<=nPoints; n++)
			{
				p[n] = new PointF(X(xVal), ToFloat(y(xVal)));
				xVal *= ratio;
			}

			if (size != 0)
				gr_.Clip = new Region(new RectangleF(0, y0, Width, size));

			gr_.SmoothingMode = mode;
			Draw(aPen, p);						// 折れ線または曲線の描画
			gr_.SmoothingMode = SmoothingMode.Default;
			gr_.Clip = new Region();			// 描画領域の限定を解除
		}

		// 10 のべき乗に非常に近い値かどうかの検査
		protected bool IsPow10(float x) => Abs(Log10(x) - Round(Log10(x))) < 0.01;
	}

	//-------------------------------------------------------------------
	// 横軸，縦軸がログスケールのグラフを描画するためのクラス
	//		LogScaleX クラスを継承する
	//-------------------------------------------------------------------
	public class LogScaleXY : LogScaleX
	{
		private readonly Values y_;		// y 軸用
		private readonly double eps_;	// Y() で値が 0 の場合に置換える値

		// コンストラクタ
		//		u	横軸に対する Values 構造体
		//		v	縦軸に対する Values 構造体
		public LogScaleXY(Graphics gr, Values u, Values v) : base(gr, u)
		{
			y_ = v;
			eps_ = 1.0e-4*y_.min;
			ToFloat = x => Y(x);
		}

		// y 座標値の取得（y <= 0 の場合でも例外は発生しない）
		public float Y(double y) => -y_.dec*(float)Log10(((y<eps_)? eps_ : y)/y_.min);

		// y 軸の数値に対する目盛線の描画
		//		h1, h2 の設定方法は DrawAxisX() と同じ
		public void DrawAxisY(Pen aPen, float w1, float w2 = 0)
		{
			if (w2 == 0) w2 = w1;
			DrawScale(y_, y => gr_.DrawLine(aPen, 0, Y(y),
											(IsPow10(y) ? w1 : w2), Y(y)));
		}

		// y 軸の数値に対する dB に対応する目盛線の描画
		//		db	目盛線の間隔に対応する dB 値，20 dB：10倍に対応
		public void DrawAxisYdB(Pen aPen, float db, float w0 = 0)
		{
			int div = (int)Round(20.0f*Log10(y_.max/y_.min)/db);
			float dy = (Y(y_.max) - Y(y_.min))/div;
			if (w0 == 0) w0 = Width;
			for (int n=0; n<=div; n++)
				gr_.DrawLine(aPen, 0, Y(y_.min)+n*dy, w0, Y(y_.min)+n*dy);
		}
	}
}
