///////////////////////////////////////////////
//	Boids_for_CS.cs
//	Copyright (c) 2026 by Y-sama
//	C#プログラム及び日本語コメント文責：Y-sama
//	Original program Copyright by Ben Eater
//	https://eater.net/boids
///////////////////////////////////////////////
using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;		//Graphicsクラス（アンチエイリアス）使用の為
using System.Collections.Generic;	//List使用の為

namespace Boids_for_CS
{
	//グローバル定数
	public enum BoidType	//Boidの種を行動特性別に以下4種とする
	{
		Flocker,			//0 - "Birds of a feather flock together"
		Introvert,			//1 - Harmless "Lone wolf" but tends to get away from a flock
		Sociopath,			//2 - Dangerous, often times kills others
		Dead				//3 - a dead body
		//https://gigazine.net/news/20240921-difference-between-psychopath-sociopath/
	}

	/////////////////////////////////////////////////////////
	//Boid（個体）クラス定義
	//注意：Boidタイプが「死骸（BoidType.Dead）」の場合、
	//（１）呼び出し側でメソッド呼び出しを制限することが原則
	//（２）気になる人は
	//		"if(bType == BoidType.Dead)	return;
	//		を行動メソッドの冒頭に追加してください
	/////////////////////////////////////////////////////////
	public class Boid						//個体
	{
		//定数
		const int VisualRange = 75;	//視野
		//プロパティ
		public int ID {private set; get;}	//BoidのID（読み取り専用）
		//フィールド定義
		public BoidType bType;				//生物学的種のID（ユーザー指定可）
		public int X;						//x座標
		public int Y;						//y座標
		public double dx;					//x移動距離（-5.0〜5.0−左右へ5.0）
		public double dy;					//y移動距離（-5.0〜5.0−上下へ5.0）
		public Queue<Point> History;		//履歴（x、y座標の履歴配列）

		//コンストラクター
		public Boid(int id)
		{
			ID = id;				//IDを指定して生成する
			History = new Queue<Point>();
		}

		//擬鳥間の距離計算（√(x軸距離 ^ 2 + y軸距離 ^ 2)）
		public int DistanceFrom(Boid other)
		{
			return (int)Math.Sqrt((other.X - this.X) * (other.X - this.X) +
								(other.Y - this.Y) * (other.Y - this.Y));
		}

		//擬鳥の移動と飛行跡記録
		//特記：Sociopathの場合、他のBoidに追いつけば殺害する
		public void Move(List<Boid> boids)
		{
			//x軸方向へdx、y軸方向へdy移動する（MidpointRounding.AwayFromZero→例「-2.5→-3.0」）
			this.X += (int)Math.Round(this.dx, MidpointRounding.AwayFromZero);
			this.Y += (int)Math.Round(this.dy, MidpointRounding.AwayFromZero);
			//50個までの要素数でキューを管理
			History.Enqueue(new Point(this.X, this.Y));	//Queueに飛行跡データを追加する
			if(History.Count > 50)						//Queueの数が50を超えたら
				History.Dequeue();						//一番古いものを削除する
			//BoidType.Sociopathの殺害行為
			if(bType == BoidType.Sociopath)
			{
				foreach(Boid otherBoid in boids)
				{
					//自分以外の他のBoidに追いついたならば
					if(otherBoid != this && DistanceFrom(otherBoid) == 0)
					{
						//そのBoidを殺害する
						otherBoid.bType = BoidType.Dead;
					}
				}
			}
		}

		//擬鳥を描画領域内に止め、境界に接近すると反対方向へ向かわせる（dx、dyは移動方向・距離）
		//特記：全BoidType共通
		public void KeepWithinBounds(int width, int height)
		{
			const int margin = 200;		//余白（調整対象）
			if(X < margin)				//x軸左方向
				dx++;					//右方向（加算）
			if(X > width - margin)		//x軸右方向
				dx--;					//左方向（減算）
			if(Y < margin)				//y軸上方向
				dy++;					//下方向（加算）
			if(Y > height - margin)		//y軸下方向
				dy--;					//上方向（減算）
		}

		//擬鳥の集団への対応（向かう｜離れる）速度を少し調節する
		//特記：FlockerとSociopathは擬鳥集団の中央へ向かうが、Introvertは離れる
		public void FlyTowardsCenter(List<Boid> boids)
		{
			const double centeringFactor = 0.005;		//速度変更係数
			int centerX = 0, centerY = 0, numNeighbors = 0;
			foreach(Boid otherBoid in boids)
			{
				//視野内の擬鳥のx、y座標とその数を加算
				if(otherBoid != this)
				{
					if(DistanceFrom(otherBoid) < VisualRange)
					{
						centerX += otherBoid.X;
						centerY += otherBoid.Y;
						numNeighbors++;
					}
				}
			}
			if(numNeighbors > 0)
			{
				//視野内の擬鳥のx、y座標の平均を求め、dx、dyを速度変更係数を乗じて調整する
				double sign = (bType != BoidType.Introvert) ? 1.0 : -1.0;	//死骸対応はしていない
				dx += (Math.Round((double)centerX / numNeighbors, MidpointRounding.AwayFromZero) - X) * centeringFactor * sign;
				dy += (Math.Round((double)centerY / numNeighbors, MidpointRounding.AwayFromZero) - Y) * centeringFactor * sign;
			}
		}

		//衝突しそうな近くの擬鳥から離れる
		//特記：FlockerとIntrovertは離れるが、Sociopathは逆に近づく
		public void AvoidOthers(List<Boid> boids)
		{
			const int minDistance = 20;					//離反する境界距離
			const double avoidFactor = 0.05;			//速度変更係数
			double moveX = 0.0, moveY = 0.0;
			foreach(Boid otherBoid in boids)
			{
				if(otherBoid != this)
				{
					//境界距離以内の擬鳥との離反（マイナス）座標を累計する
					if(DistanceFrom(otherBoid) < minDistance)
					{
						//Sociopathでない自分が
						if(bType != BoidType.Sociopath)
						{	//Sociopathと遭遇した場合
							if(otherBoid.bType == BoidType.Sociopath)
							{	//必死に（通常の倍の加速で）逃げようとする
								dx += (X - otherBoid.X) * avoidFactor * 2;
								dy += (Y - otherBoid.Y) * avoidFactor * 2;
								return;
							}
							//相手がSociopathでなければ避けようとする
							else
							{
								moveX += X - otherBoid.X;	//-(otherBoid.X - X)
								moveY += Y - otherBoid.Y;	//-(otherBoid.Y - Y)
							}
						}
						//自分がSociopathなら
						else
						{	//相手に近づこうとする
							moveX += otherBoid.X - X;
							moveY += otherBoid.Y - Y;
						}
					}
				}
			}
			//自分の方向、速度（dx、dy）に離反・接近速度（速度変更係数を乗じたもの）を反映
			dx += moveX * avoidFactor;
			dy += moveY * avoidFactor;
		}

		//他の擬鳥の速さ（速度と方向）を求め、それへ自分を調整する
		//特記：FlockerとIntrovertは離れるが、Sociopathは逆に近づく
		public void MatchVelocity(List<Boid> boids)
		{
			//Flockerは群れに歩調を合わせるが、IntrovertとSociopathは無視する
			if(bType != BoidType.Flocker)
				return;
			const double matchingFactor = 0.05;			//速度変更係数
			double avgDX = 0.0, avgDY = 0.0;
			int numNeighbors = 0;
			foreach(Boid otherBoid in boids)
			{
				if(otherBoid != this)
				{
					//視野内の擬鳥のx、y移動距離を加算し、その数を記録
					if(DistanceFrom(otherBoid) < VisualRange)
					{
						avgDX += otherBoid.dx;
						avgDY += otherBoid.dy;
						numNeighbors++;
					}
				}
			}
			//移動距離の合計を総数で除算して平均を求め、自分の移動距離との差に速度変更係数を乗じたもので調整する
			if(numNeighbors > 0)
			{
				avgDX = avgDX / numNeighbors;
				avgDY = avgDY / numNeighbors;
				dx += (avgDX - dx) * matchingFactor;
				dy += (avgDY - dy) * matchingFactor;
			}
		}

		//速度（dx、dyのベクトル値）は変化するが15を超えない
		public void LimitSpeed()
		{
			const double speedLimit = 15.0;			//限界速度
			//x、y軸の移動距離から求められる速度（speed）が
			double speed = Math.Sqrt(dx * dx + dy * dy);
			//限界速度を超えたならば、x、y軸の移動距離を限界速度で頭打ちにする
			if(speed > speedLimit)
			{
				dx = speedLimit / speed * dx;
				dy = speedLimit / speed * dy;
			}
		}

		//擬鳥の描画
		public void Draw(Graphics gHandle, bool draw_trail = false)
		{
			if(bType == BoidType.Dead)	return;			//死骸は（明示的に）描画しない
			//Boidの描画色
			Color[] BoidCol = new Color [3] {Color.Blue, Color.Green, Color.Red};
			//飛行跡のPen色
			Pen[] BoidTrail = new Pen[3] {Pens.SkyBlue, Pens.LightGreen, Pens.Tomato};
			//半直線(0, 0)-(x, y)と正のx軸の平面上の角度（ラジアン単位）
			//float angle = (float)Math.Atan2(dy, dx);
			float angle = (float)(Math.Atan2(dy, dx) * 180.0 / Math.PI);
			//三角形の3つの頂点（原点(0,0)周辺に定義）
			PointF[] points = {
				new PointF(8, 0),	// 上の頂点
				new PointF(-8, -4),	// 左下の頂点
				new PointF(-8, +4)	// 右下の頂点
			};
			//原点の移動と回転
			gHandle.TranslateTransform(X, Y);			//描画位置(X, Y)へ移動
			gHandle.RotateTransform(angle);
			//塗りつぶし実行
			using(Brush brush = new SolidBrush(BoidCol[(int)bType]))
			{
				gHandle.FillPolygon(brush, points);
			}
			//Graphicsの状態をリセット
			gHandle.ResetTransform();
			//飛行跡が真であれば一定の長さの飛行痕（糸のような線）を残す
			if(draw_trail)
			{
				//飛行跡（Point）配列をDrawLinesで描く
				Point[] trail = this.History.ToArray();
				if(trail.Length > 1)
				{
					//gHandle.DrawLines(Pens.SkyBlue, trail);
					gHandle.DrawCurve(BoidTrail[(int)bType], trail);
				}
			}
		}
	}

	/////////////////////////////////////////////////
	//Boids（個体群）クラス定義（Boidsコントロール）
	/////////////////////////////////////////////////
	public class Boids : Control			//種の群れ
	{
		//フィールド定義
		private static Random rand;			//乱数
		private List <Boid> boids;			//擬鳥配列
		private Image bkImage;				//背景画像（nullか否かで背景の有無を判断）
		//描画関係プロパティ
		private Bitmap Canvas {set; get;}	//仮想画面ビットマップ
		private Graphics gHandle {set; get;}//仮想画面のグラフィック
		private Timer DrawTimer;			//描画用タイマー
		private bool Draw_Trail = false;	//飛行跡許可フラグ

		public Boids()
		{
			//乱数の初期化
			rand = new Random((int)DateTime.Now.Ticks);
			//擬鳥配列の生成
			boids = new List<Boid>();
			//タイマー初期設定
			DrawTimer = new Timer();
			DrawTimer.Interval = 15;		//設定値15/1000秒（最小値は事実上15程度）
			DrawTimer.Tick += DrawBoids;
			//コントロール生成時処理
			//ちらつき防止
			SetStyle(
				ControlStyles.UserPaint |
				ControlStyles.AllPaintingInWmPaint |
				ControlStyles.OptimizedDoubleBuffer,
				true
			);
			UpdateStyles();
		}

		//終了時リソース開放処理
		protected override void Dispose(bool disposing)
		{
			if(disposing)
			{
				if(DrawTimer != null)
					DrawTimer.Dispose();	//タイマーの廃棄
				if(gHandle != null)
					gHandle.Dispose();		//描画ハンドルの廃棄
				if(Canvas != null)
					Canvas.Dispose();		//仮想画面の廃棄
				if(bkImage != null)
					bkImage.Dispose();		//背景画像の廃棄
			}
			base.Dispose(disposing);
		}

		//Resizeイベント処理
		protected override void OnResize(EventArgs e)
		{
			base.OnResize(e);
			//描画Canvas（再）設定
			InitCanvas();
		}

		//Paintイベント処理
		protected override void OnPaint(PaintEventArgs e)
		{
			base.OnPaint(e);
			//Canvas描画処理
			if(Canvas != null)	//仮想画面がnullの場合はスキップ
				e.Graphics.DrawImage(Canvas, 0, 0);
		}

		//描画Canvas初期化・再設定
		private void InitCanvas()
		{
			//コントロール生成中のエラー対策
			if(this.ClientSize.Width < 1 || this.ClientSize.Height < 1)
				return;
			//既存の仮想画面、グラフィックハンドルがあれば廃棄する
			if(gHandle != null)
			{
				gHandle.Dispose();
				gHandle = null;
			}
			if(Canvas != null)
			{
				Canvas.Dispose();
				Canvas = null;
			}
			//ビットマップ仮想画面の作成
			Canvas = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
			//Graphicsクラス（アンチエイリアス）
			gHandle = Graphics.FromImage(Canvas);
			gHandle.SmoothingMode = SmoothingMode.AntiAlias;
			gHandle.PixelOffsetMode = PixelOffsetMode.HighQuality;
			//背景描画
			if(bkImage != null)
				gHandle.DrawImage(bkImage, 0, 0);
		}


		//Boidsの描画（Timer割り込み処理）
		private void DrawBoids(object sender, EventArgs e)
		{
			if(bkImage != null)			//背景描画
				gHandle.DrawImage(bkImage, 0, 0);
			else						//描画面を消去
				gHandle.Clear(this.BackColor);
			foreach(Boid boid in boids)
			{
				if(boid.bType == BoidType.Dead)
					continue;					//死骸は何もしない
				//速さをそれぞれの規則に基づいて更新
				boid.FlyTowardsCenter(boids);	//群れへの参集
				boid.AvoidOthers(boids);		//近すぎる擬鳥からの離反
				boid.MatchVelocity(boids);		//速度調整
				boid.LimitSpeed();				//限界速度制限
				//Boidsコントロールの枠内にとどめる
				boid.KeepWithinBounds(this.ClientSize.Width, this.ClientSize.Height);
				//現在の速さに基づき位置（x、y座標）を更新し、飛行跡を追加
				boid.Move(boids);
				//Boidsの描画
				boid.Draw(gHandle, Draw_Trail);	//Draw_Trail−飛行跡の描画指定
			}
			//Canvasを表示
			this.Invalidate();
		}

		//背景の設定（ユーザーがPCディスプレイのサイズに合わせた画像を自由に設定する）
		public void SetBackGround(bool flag)
		{
			if(!flag)
			{
				MessageBox.Show("背景を無効にします。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Information);
				if(bkImage != null)		//元々nullであれば何もしない
				{
					bkImage.Dispose();
					bkImage = null;
				}
			}
			else
			{
				//OpenFileDialogオブジェクトの生成
				OpenFileDialog openDlg = new OpenFileDialog();	//usingを使うと最後のDispose処理が不要になる
				openDlg.Filter = "イメージファイル（png, jpeg/jpg, bmp, gif）|*.png;*.jpeg;*.jpg;*.bmp;*.gif";
				DialogResult result = openDlg.ShowDialog();
				if(result == DialogResult.OK)
				{
					if(bkImage != null)	//既に背景が設定されていた場合、一旦廃棄する
					{
						bkImage.Dispose();
						bkImage = null;
					}
					bkImage = Image.FromFile(openDlg.FileName);
				}
				else if(result == DialogResult.Cancel)
					MessageBox.Show("キャンセルされました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Information);
				//オブジェクトを破棄する
				openDlg.Dispose();
			}
		}

		//擬鳥の生成（擬鳥配列の初期化）
		public string CreateBoids(int count, bool lonewolf = false, bool sociopath = false)
		{
			//コントロール生成中のエラー対策（エラーは空文字を返す）
			if(this.ClientSize.Width < 1 || this.ClientSize.Height < 1)
				return String.Empty;
			//一旦boids（List）を空にする
			boids.Clear();
			//戻り値文字列
			string ResMsg = "Boidsの総数：" + count.ToString();
			//擬鳥配列の初期化
			for(int i = 0; i < count; i++)
			{
				//擬鳥初期化
				Boid bd = new Boid(i);			//iがIDとなる
				bd.bType = BoidType.Flocker;	//一種だけなので暫定的にFlockerとする
				bd.X = rand.Next(this.ClientSize.Width);	//0〜Width - 1
				bd.Y = rand.Next(this.ClientSize.Height);	//0〜Height - 1 ;
				bd.dx = rand.Next(-5, 6);		//-5.0〜5.0−左右へ5.0
				bd.dy = rand.Next(-5, 6);		//-5.0〜5.0−上下へ5.0
				bd.History.Enqueue(new Point(bd.X, bd.Y));	//最初の位置を記録
				boids.Add(bd);
			}
			//Introvert（総数の5%程度）を付加する
			int numInt = 0;
			if(lonewolf)
			{
				int num = rand.Next(1, (int)(count * 0.05) + 1);	//1〜Count x 5%までの乱数値
				numInt = num;
				while(num > 0)	//総数に対して選択数が小さいのでシャッフル方式は採らない
				{
					int n = rand.Next(count);
					if(boids[n].bType == BoidType.Flocker)
					{
						boids[n].bType = BoidType.Introvert;
						num--;
					}
				}
			}
			//Sociopath（総数の3%程度）を付加する
			int numSocio = 0;
			if(sociopath)		//総数に対して選択数が小さいのでシャッフル方式は採らない
			{
				int num = rand.Next(1, (int)(count * 0.03) + 1);	//1〜Count x 3%までの乱数値
				numSocio = num;
				while(num > 0)	//総数に対して選択数が小さいのでシャッフル方式は採らない
				{
					int n = rand.Next(count);
					if(boids[n].bType == BoidType.Flocker)
					{
						boids[n].bType = BoidType.Sociopath;
						num--;
					}
				}
			}
			ResMsg += "（Flocker：" + (count - numInt - numSocio).ToString() + "、Introvert：" + numInt.ToString() + "、Sociopath：" + numSocio.ToString() + "）";
			return ResMsg;
		}

		//Boids開始
		public bool Start()
		{
			//擬鳥未生成の場合のエラートラップ
			if(boids.Count == 0)
			{
				MessageBox.Show("まだ擬鳥が生成されていません。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			//描画不可状態でのエラートラップ
			if(Canvas == null || gHandle == null)
			{
				MessageBox.Show("Graphics not ready.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
			DrawTimer.Start();				//タイマー割り込み開始
			return true;
		}

		//Boids停止
		public void Stop()
		{
			DrawTimer.Stop();				//タイマー割り込み停止
		}

		//飛行跡描画可否
		public void DrawTrail(bool flag)
		{
			Draw_Trail = flag;				//飛行跡描画指定
		}

		//Boids結果表示
		public void Result()
		{
			string ResMsg = "Boidsの総数：" + boids.Count.ToString();
			int numFlock = 0,  numInt = 0,  numSocio = 0;
			foreach(Boid boid in boids)
			{
				switch((int)boid.bType)
				{
					case 0:		//Flocker
						numFlock++;
						break;
					case 1:		//Introvert
						numInt++;
						break;
					case 2:		//Sociopath
						numSocio++;
						break;
					default:	//Dead
						break;
				}
			}
			ResMsg += "（Flocker：" + numFlock.ToString() + "、Introvert：" + numInt.ToString() + "、Sociopath：" + numSocio.ToString() + "、Dead：" + (boids.Count - numFlock - numInt - numSocio).ToString() + "）";
			MessageBox.Show(ResMsg, "Boidsの結果", MessageBoxButtons.OK, MessageBoxIcon.Information);
		}
	}
}
