//------------------------------------------------------------------------------
//	重み付きチェビシェフ近似による直線位相 FIR フィルタの設計，Ver. 3.4.7
//	（Parks-McClellan のアルゴリズムによる）
//		T. W. Parks and C. S. Burrus: “Digital filter design,”
//		Chapter 3, Joh Wiley & Sons, Inc., 1987. 参照
//
//	DPI の異なるシステムへの対応を行った
//	ログスケール表示の際の周波数範囲の設定の不具合を修正
//	C# ver.7 の新機能も使うようにした
//
//	2017/06/15	VisualStudio 2017 でビルドをし直し，問題のないことを確認済み
//
//	2019/09/25	周波数特性の表示の際に，Graphics.DrawCurve() をやめ
//				Graphics.DrawLines() を使うように変更した．
//
//	2019/09/26	表示の不具合を修正した．
//
//	2022/01/04	利得曲線の描画範囲の上限を上方向へ修正した
//				遮断周波数を表す直線の描画範囲を利得曲線の描画範囲内に制限した
//
//	2022/12/07	インデックス範囲外のアクセスの例外も補足するようにした
//				それ以上実行できなくなった場合のメッセージを修正した
//
//	2023/03/29	設計パラメータを入力する際に，微分器，ヒルベルト変換器を設計す
//				る場合に使わない項目には，入力できないように修正した．
//
//		作成者：三上直樹，2023/04/03 作成，(C)三上直樹 2023
//------------------------------------------------------------------------------

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

using Mikami.WindowsApplication;
using Mikami.GraphicsLib;
using Mikami.Remez;

namespace FIR_Design_Remez
{
	public partial class Form1 : Form
	{
		private const int MAX_BANDS_ = 5;		// 設計可能な最大の帯域の数
		private const int MAX_ORDER_ = 500;		// 設計可能な最大の次数
		private const int F_DIV_ = 1024;		// 振幅特性描画の際の点数
		private const double A_MIN_ = 1.0e-10;	// dB 値の最小値に対応する値
		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 const int DRAW_MAX_ = 10;		// 描画範囲の上限
		private const int DRAW_MIN_ = 8*Y_DB_+DRAW_MAX_;	// DRAW_MAX_ を基準とする描画範囲の下限

		private Label[] labelBand_;
		private TextBox[,] parameters_;
		private LogScaleXY yLog_;	// 周波数ログスケール表示用
		private float frqL_;		// 周波数ログスケール表示の際の下端の周波数
		private float frqH_;		// 周波数ログスケール表示の際の下端の周波数

		private double[] hk_;								// 係数
		private float[] logA_k_ = new float[F_DIV_+1];		// 振幅特性の値
		private double[] bands_ = new double[2*MAX_BANDS_];	// 帯域端周波数
		private double[] gain_ = new double[MAX_BANDS_];	// 利得
		private double[] weight_ = new double[MAX_BANDS_];	// 重み
		private	double[] deviat_ = new double[MAX_BANDS_];	// 偏差

		private double fSkHz_;		// 標本化周波数 [kHz]
		private double fSample_;	// 標本化周波数
		private int	   nOrder_;		// 次数
		private int    nBand_;		// 帯域の数

		private Remez.FType type_ = Remez.FType.passStop;
		private ToolTip toolTip1;

		private readonly float dx_;			// 特性描画の際の横方向間隔（ピクセル数）
		private readonly float dpiFactor_;	// DPI 調整のための係数

		public Form1()
		{
			InitializeComponent();

			this.MinimumSize = this.Size;	// 起動時のウィンドウのサイズをサイズの最小値に設定
			CommonProc.Disable(pictureBox1, richTextBox1, button3, label12, label13, label14,
							   textBox4, textBox5);
			CommonProc.Do(c => c.Visible = false, textBox3, label9, label10, comboBox1, button5);
			FormCloseConfirm.Set(this);		// アプリケーション終了の際の確認の設定
			EnterToTab.Set(this);			// Enter キーを Tab キーのように使う設定
			type_ = Remez.FType.passStop;	// デフォルトは通過域/阻止域型
			comboBox1.SelectedIndex = 4;	// -80 dB まで表示する設定

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

			textBox2.ToDouble(out fSkHz_);	// 標本化周波数のデフォルト値設定
			textBox1.Select();
			KeyPreview = true;				// キーイベントをフォームが先に受け取る
		}

		private void Form1_Load(object sender, EventArgs e)
		{
			// 2次元配列の TextBox で使う ToolTip の初期化
			toolTip1 = new ToolTip();

			CreateLabelArray(MAX_BANDS_, Dpi(194), this.ClientSize.Height - Dpi(114));
			CreateTextBoxMatrix(MAX_BANDS_, 4, Dpi(176), this.ClientSize.Height - Dpi(94));
			InitTextBoxes();
			BandEnableOnOff(2);
		}

		// ｢設計｣ ボタン
		private void button1_Click(object sender, EventArgs e)
		{
			if (!SpecInput()) return;
			Remez design = new Remez(nOrder_, out hk_);
			if (!design.Do(nBand_, bands_, gain_, weight_, deviat_, type_, fSkHz_))
			{
				MessageBox.Show("与えたパラメータが不適切です．パラメータの与え方を変えてください．");
				return;
			}

			CommonProc.Enable(pictureBox1, richTextBox1, button3, button5,
							  label9, label10, textBox3);
			CommonProc.Do(c => c.Visible = true, textBox3, label9, label10,
						  comboBox1, button5);
	
			OutputToRitchTextBox();		// 係数の出力			
			CalcAmp(ref logA_k_);		// 振幅特性の計算

			pictureBox1.Invalidate();
		}

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

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

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

		// ｢入力｣ ボタン：指定された周波数における利得の表示
		private void button5_Click(object sender, EventArgs e)
		{
			if (!textBox3.ToDouble(out double fr,
								   v => (v >= 0.0) && (v <= fSkHz_/2),
								   "周波数を 0 以上，標本化周波数／2 以下に設定してください．"
								   )) return;	// 遮断周波数
			
			// fr に対する振幅特性の値を計算する
			double amp = HwA(2.0*Math.PI*fr/fSkHz_);

			label10.Text = (amp > 1.0E-10) ?
							(20.0*Math.Log10(amp)).ToString("0.000") + " dB"
							: "-∞ dB";
			textBox3.Select();
			textBox3.SelectAll();
		}

		// 周波数特性の表示
		private void pictureBox1_Paint(object sender, PaintEventArgs e)
		{
			if (!pictureBox1.Enabled) return;

			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 (radioButton7.Checked) DrawLinear(gr, myFont, myBrush);
			if (radioButton8.Checked) DrawLog(gr, myFont, myBrush);
		}

		// 次数の入力
		private void textBox1_Leave(object sender, EventArgs e)
		{
			SpecInput();
			CommonProc.Disable(richTextBox1, button3, button5);
		}

		// 標本化周波数の入力
		private void textBox2_Leave(object sender, EventArgs e)
		{
			if (!textBox2.ToDouble(out fSkHz_, x => x > 0,
				"標本化周波数は正の値にしてください．")) return;
			parameters_[1, nBand_-1].Text = (fSkHz_/2).ToString();
			CommonProc.Disable(richTextBox1, button3, button5);

			textBox4.Text = (fSkHz_/100).ToString("0.###");
			textBox5.Text = (fSkHz_/2).ToString("0.###");
		}

		// 帯域の数が変化したときの処理
		private void numericUpDown1_ValueChanged(object sender, EventArgs e)
		{
			BandEnableOnOff((int)numericUpDown1.Value);
			parameters_[1, nBand_-1].Text = (fSkHz_/2).ToString();
			CommonProc.Disable(richTextBox1, button3, button5,
							   label9, label10, textBox3);
		}

		// フィルタのタイプが変化したときの設定
		private void radioButton1_Click(object sender, EventArgs e)
		{
			// フィルタのタイプ設定
			if (radioButton1.Checked) type_ = Remez.FType.passStop;
			if (radioButton2.Checked) type_ = Remez.FType.diff;
			if (radioButton3.Checked) type_ = Remez.FType.hilbert;
			
			// 微分器/ヒルベルト変換器の場合の上側帯域端周波数のデフォルト値
			if (type_ != Remez.FType.passStop)
				parameters_[1, 0].Text = (fSkHz_/2).ToString();
			
			int nBand = (type_ != Remez.FType.passStop) ? 1 : (int)numericUpDown1.Value;
			if (nBand == 1) CommonProc.Disable(numericUpDown1, label2);
			else			CommonProc.Enable(numericUpDown1, label2);
			BandEnableOnOff(nBand);
			TextBoxCtrl();
			CommonProc.Disable(richTextBox1, button3, button5,
							   label9, label10, textBox3);
		}

		// 出力形式を切り替えた場合の対応
		private void radioButton4_Click(object sender, EventArgs e)
		{
			// 係数の出力
			if (button3.Enabled) OutputToRitchTextBox();
		}
		
		// 周波数特性の表示の際の周波数軸の切り替え
		private void radioButton7_CheckedChanged(object sender, EventArgs e)
		{
			if (radioButton8.Checked)
			{
				CommonProc.Enable(textBox4, textBox5, label12, label13, label14);
				SetDisplayRange();
			}
			else
				CommonProc.Disable(textBox4, textBox5, label12, label13, label14);
			pictureBox1.Invalidate();
		}

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

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

		// textBox1, textBox2 内容が書き換えられた場合に対応
		private void textBox1_TextChanged(object sender, EventArgs e)
		{
			CommonProc.Disable(richTextBox1, button3);
		}

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

		// カーソルで指定した周波数における利得を表示する
		private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
		{
			if (!button3.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 (radioButton7.Checked)
				fr = (e.X - Dpi(X0_))*(fSkHz_/2.0)/Dpi(5*X_FR_);
			else
				fr = 0.001*yLog_.ToFrq(e.X/dpiFactor_ - X0_);
			textBox3.Text = fr.ToString("0.0000");
			button5_Click(sender, null);
		}

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

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

		// 1 行のデータ数を変えたときの対応
		private void numericUpDown2_ValueChanged(object sender, EventArgs e)
		{
			if (button3.Enabled) OutputToRitchTextBox();
		}

		// 異なる DPI への対応（expression-bodied function）
		private int Dpi(float x) => (int)Math.Round(x*dpiFactor_);
	}
}
