﻿//------------------------------------------------------------------------------
//	双一次ｚ変換による IIR フィルタの設計，Ver. 3.4.5
//		設計可能なフィルタ：Butterworth, Chebyshev, Inverse Chebyshev, Elliptic
//
//		T. W. Parks and C. S. Burrus : “Digital Filter Design,”
//		Chapter 7, John Wiley & Sons, 1987 参照
//
//	DPI の異なるシステムへの対応を行った
//	ログスケール表示の際の周波数範囲の設定の不具合を修正
//
//	2017/06/15 に VisualStudio 2017 でビルドをし直し，問題のないことを確認済み
//
//	2020/08/09 に VisualStudio 2019 でビルドをし直し，問題のないことを確認済み
//
//	2022/01/04	LogScaleEx.cs の一部を変更
//				遮断周波数を表す直線の描画範囲を利得曲線の描画範囲内に制限した
//
//	2022/12/13	係数を float 型の精度で出力する際，小数点以下 8 桁とした
//
//	2023/03/31	逆チェビシェフ特性で奇数次の不具合などを修正した
//
//	2023/04/01	利得定数を縦続形の係数から求めるように変更した
//
//	2023/06/23	BPF，BRF を設計した後，HPF を何回か設計するとエラーになる点を
//				修正した
//
//		作成者：三上直樹，2023/06/23 作成，©三上直樹 2023
//------------------------------------------------------------------------------

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Numerics;
using System.IO;

using Mikami.WindowsApplication;
using Mikami.GraphicsLib;

using static System.Math;

namespace IIR_Design
{
	public partial class Form1 : Form
	{
		// 縦続形の係数の対応する構造体
		private struct Coefs { public double a1, a2, b0, b1, b2; }

		private const int MAX_ORDER_ = 20;	// 最大の次数（BPF, BRF はこの半分）
		private const int F_DIV_ = 1024;	// 振幅特性描画の際の点数
		private const int X_FR_ = 102;		// x軸の1目盛り（(fs/2)/5）
		private const int Y_DB_ =  56;		// y軸の1目盛り（表示dBの幅/8）
		private const int X0_ = 68;			// スペクトル表示原点の x 座標
		private const int Y0_ = 14;			// スペクトル表示原点の y 座標

		private Complex[] s_pz_;			// ｓ平面の極・零点
		private Complex[] z_pz_;			// ｚ平面の極・零点
		private double[]  ak_;				// 直接形に対応する伝達関数の分母の係数
		private double[]  bk_;				// 直接形に対応する伝達関数の分子の係数
		private Coefs[]	  cm_;				// 縦続形の係数
		private Coefs[]	  cmS_;				// 縦続形の係数（スケーリングされた係数）
		private double	  gain_;			// 縦続形の利得定数
		private double[]  scaleC_;			// スケーリング係数
		private double	  fSample_;			// 標本化周波数
		private double	  fSkHz_;			// 標本化周波数 [kHz]
		private double	  fcLow_;			// 遮断周波数（低域側）
		private double	  fcHigh_;			// 遮断周波数（高域側）
		private double	  dbRrpl_;			// 通過域のリップル
		private double	  dbD_; 			// 阻止域の減衰量
		private int		  zOrd_;			// 伝達関数の次数

		private LogScaleXY yLog_;			// 対数スケール表示用
		private float[] logA_k_ = new float[F_DIV_+1];	// 計算された対数振幅特性の値
		private float frqL_;	// 周波数ログスケール表示の際の下端の周波数
		private float frqH_;	// 周波数ログスケール表示の際の下端の周波数

		private readonly float dx_;				// 特性描画の際の横方向間隔（ピクセル数）
		private readonly float dpiFactor_;		// DPI 調整のための係数
		private readonly RadioGroup formType_;	// groupBox1 の RadioButton に対応
		private readonly RadioGroup passBand_;	// groupBox2 の RadioButton に対応

		public Form1()
		{
			InitializeComponent();

			pictureBox1.Paint -= pictureBox1_Paint;
			this.MinimumSize = this.Size;	// 起動時のウィンドウのサイズをサイズの最小値に設定

			formType_ = new RadioGroup(groupBox1, s => s.Substring(0, 1));	// 先頭の１文字を使う
			passBand_ = new RadioGroup(groupBox2);							// 文字を全部使う

			dx_ = X_FR_*5.0f/F_DIV_;
			dpiFactor_ = (this.CreateGraphics().DpiX)/96.0f;

			comboBox1.SelectedIndex = 4;	// -80 dB まで表示
			EnableCtrlShape();
			EnableCtrlBand();
			textBox2.ToDouble(out fSkHz_);	// fSkHz_ の初期化

			FormCloseConfirm.Set(this);		// 終了時に確認できるようにする
			EnterToTab.Set(this);			// ｢Enter キー｣ を ｢Tab キー｣ のように使う
			textBox1.Select();

			CommonProc.Disable(button2, button4, textBox8, textBox9, label14, label15, label16);
			CommonProc.Do(c => c.Visible = false, textBox7, button5, label12, label13,
						  comboBox1);

			KeyPreview = true;				// キーイベントをフォームが先に受け取る
		}

		// ｢設計｣ ボタン
		private void button1_Click(object sender, EventArgs e)
		{
			if (!ParmInput(out int order, out double eps, out double scale, out double delta)) return;

			Design(order, delta, scale, eps);

			CommonProc.Enable(richTextBox1 ,button2, button4, button5,
							  textBox7, label13);
			CommonProc.Do(c => c.Visible = true, textBox7, button5, label12, label13,
						  comboBox1, panel1);

			// 係数の出力
			XferFuncOut(richTextBox1);
			CalcAmp(ref logA_k_);
			textBox7.Text = (fcLow_ == 0.0) ? (fcHigh_/1000.0).ToString()
											: (fcLow_/1000.0).ToString();
			button5_Click(sender, e);

			pictureBox1.Paint -= pictureBox1_Paint;
			pictureBox1.Paint += pictureBox1_Paint;
			pictureBox1.Invalidate();
		}

		// ｢スケーリング｣ ボタン
		private void button2_Click(object sender, EventArgs e)
		{
			Scaling();
			CascadeOut(richTextBox1, "スケーリングされた縦続形の係数", scaleC_[0], cmS_);
			button2.Enabled = false;
		}

		// ｢説明｣ ボタン
		private void button3_Click(object sender, EventArgs e)
		{
			button3.Enabled = false;
			Form2 myForm2 = new Form2();
			myForm2.Show(this);
		}

		// ｢結果の保存｣ ボタン
		private void button4_Click(object sender, EventArgs e)
		{
			if (saveFileDialog1.ShowDialog() == DialogResult.OK)
				richTextBox1.SaveFile(saveFileDialog1.FileName,
									  RichTextBoxStreamType.PlainText);
		}

		// ｢入力｣ ボタン：指定された周波数における利得の表示
		private void button5_Click(object sender, EventArgs e)
		{
			if (!textBox7.ToDouble(out double fr,
								   v => (v >= 0.0) && (v <= fSkHz_/2),
								   "周波数を 0 以上，標本化周波数／2 以下に設定してください．"
								   )) return;	// 遮断周波数
			double abs_hz = AbsHw(fr*2.0*PI/fSkHz_, gain_);
			if (abs_hz > 1.0E-30)
				label13.Text = ($"{20.0*Log10(abs_hz), 7:F3} dB").TrimStart(' ');
			else
				label13.Text = "-∞ dB";
			textBox7.Select();
			textBox7.SelectAll();
		}

		// ｢終了｣ ボタン
		private void button6_Click(object sender, EventArgs e)
		{
			Close();
		}

		// フィルタの形状の指定が変化した場合の処理
		private void radioButton1_CheckedChanged(object sender, EventArgs e)
		{
			EnableCtrlShape();
			CommonProc.Disable(richTextBox1, button2, button4, button5, textBox7, label13);
		}

		// フィルタの通過／阻止帯域の指定が変化した場合の処理
		private void radioButton5_CheckedChanged(object sender, EventArgs e)
		{
			EnableCtrlBand();
			CommonProc.Disable(richTextBox1, button2, button4, button5, textBox7, label13);
		}

		// TextBox1 の内容が変更された場合の処理
		private void textBox1_TextChanged(object sender, EventArgs e)
		{
			CommonProc.Disable(richTextBox1, button2, button4, button5, textBox7, label13);
		}

		// 周波数特性の表示
		private void pictureBox1_Paint(object sender, PaintEventArgs e)
		{
			Graphics gr = e.Graphics;
			gr.ResetTransform();
			gr.ScaleTransform(dpiFactor_, dpiFactor_);	// スケール変換
			gr.TranslateTransform(X0_, Y0_);			// y軸の原点：0 dB に対応

			Font myFont = new Font("メイリオ", 9/dpiFactor_);
			Brush myBrush = Brushes.Black;

			if (radioButton11.Checked) DrawLinear(gr, myFont, myBrush);
			else					   DrawLog(gr, myFont, myBrush);
		}

		// 周波数特性の表示の際の周波数軸の切り替え
		private void radioButton11_CheckedChanged(object sender, EventArgs e)
		{
			if (radioButton12.Checked)	// ログスケールの場合
			{
				CommonProc.Enable(textBox8, textBox9, label14, label15, label16);
				SetDisplayRange();
			}
			else						// リニアスケールの場合
				CommonProc.Disable(textBox8, textBox9, label14, label15, label16);
			pictureBox1.Invalidate();
		}

		// 縦軸のスケールを変更する
		private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
		{
			pictureBox1.Invalidate();
			ProcessTabKey(true);
		}

		// ｢Enter キー｣ で指定された周波数における利得を表示する
		private void textBox7_KeyDown(object sender, KeyEventArgs e)
		{
			if (e.KeyCode == Keys.Enter) button5_Click(sender, e);
		}

		// カーソルで指定した周波数における利得を表示する
		private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
		{
			int Dpi(float x) => (int)Round(x*dpiFactor_);	// 異なる DPI への対応

			if (!button4.Enabled) return;
			if ( (e.X < Dpi(X0_)) || (e.X > Dpi(X0_ + 5*X_FR_)) ) return;
			if ( (e.Y < Dpi(Y0_)) || (e.Y > Dpi(Y0_ + 8*Y_DB_)) ) return;

			Cursor.Current = new Cursor(new MemoryStream(Properties.Resources.MyCross));
			double fr;
			if (radioButton11.Checked)
				fr = (e.X - Dpi(X0_))*(fSkHz_/2.0)/Dpi(5*X_FR_);
			else
				fr = 0.001*yLog_.ToFrq(e.X/dpiFactor_ - X0_);

			textBox7.Text = fr.ToString("0.0000");
			button5_Click(sender, null);
		}

		// 出力形式を切り替えた場合の対応
		private void radioButton9_Click(object sender, EventArgs e)
		{
			if (button4.Enabled)
			{
				XferFuncOut(richTextBox1);
				button2.Enabled = true;
			}
		}

		// F1 キーの押し下げで説明を表示する
		private void Form1_KeyDown(object sender, KeyEventArgs e)
		{
			if (e.KeyCode == Keys.F1) button3_Click(sender, null);
		}

		// 全選択
		private void textBox1_Click(object sender, EventArgs e)
		{
			((TextBox)sender).SelectAll();
		}

		// 周波数軸ログ表示で表示範囲が変更されたときの処理
		private void textBox8_Leave(object sender, EventArgs e)
		{
			SetDisplayRange();
			pictureBox1.Invalidate();
		}

		// 標本化周波数変更に対応する処理
		private void textBox2_Leave(object sender, EventArgs e)
		{
			textBox2.ToFloat(out float fs);
			textBox8.Text = (fs/100).ToString("0.###");
			textBox9.Text = (fs/2).ToString("0.###");
		}
	}
}
