using System;
using System.Globalization;
using System.Linq;
using System.Windows.Forms;

using BizHawk.Common.StringExtensions;
using BizHawk.Common.NumberExtensions;
using BizHawk.Client.Common;

namespace BizHawk.Client.EmuHawk
{
	public class WatchValueBox : TextBox, INumberBox
	{
		private WatchSize _size = WatchSize.Byte;
		private WatchDisplayType _type = WatchDisplayType.Hex;

		public WatchValueBox()
		{
			CharacterCasing = CharacterCasing.Upper;
		}

		public bool Nullable { get; set; } = true;

		public WatchSize ByteSize
		{
			get => _size;
			set
			{
				var changed = _size != value;
				
				_size = value;
				if (changed)
				{
					SetMaxLength();

					var isTypeCompatible = value switch
					{
						WatchSize.Byte => ByteWatch.ValidTypes.Any(t => t == _type),
						WatchSize.Word => WordWatch.ValidTypes.Any(t => t == _type),
						WatchSize.DWord => DWordWatch.ValidTypes.Any(t => t == _type),
						_ => false
					};

					if (!isTypeCompatible)
					{
						_type = WatchDisplayType.Unsigned;
					}

					ResetText();
				}
			}
		}

		public WatchDisplayType Type
		{
			get => _type;
			set
			{
				var val = ToRawInt();
				_type = value;
				SetMaxLength();
				SetFromRawInt(val);
			}
		}

		private uint MaxUnsignedInt =>
			ByteSize switch
			{
				WatchSize.Word => ushort.MaxValue,
				WatchSize.DWord => uint.MaxValue,
				_ => byte.MaxValue
			};

		private int MaxSignedInt =>
			ByteSize switch
			{
				WatchSize.Word => short.MaxValue,
				WatchSize.DWord => int.MaxValue,
				_ => sbyte.MaxValue
			};

		private int MinSignedInt =>
			ByteSize switch
			{
				WatchSize.Word => short.MinValue,
				WatchSize.DWord => int.MinValue,
				_ => sbyte.MinValue
			};

		private double Max12_4 => MaxUnsignedInt / 16.0;

		private double Max20_12 => MaxUnsignedInt / 4096.0;

		private double Max16_16 => MaxUnsignedInt / 65536.0;

		private static double _12_4_Unit => 1 / 16.0;

		private static double _20_12_Unit => 1 / 4096.0;

		private static double _16_16_Unit => 1 / 65536.0;

		public override void ResetText()
		{
			if (Nullable)
			{
				Text = "";
			}
			else
			{
				switch (Type)
				{
					default:
					case WatchDisplayType.Signed:
					case WatchDisplayType.Unsigned:
						Text = "0";
						break;
					case WatchDisplayType.Hex:
						Text = 0.ToHexString(MaxLength);
						break;
					case WatchDisplayType.FixedPoint_12_4:
					case WatchDisplayType.FixedPoint_20_12:
					case WatchDisplayType.FixedPoint_16_16:
					case WatchDisplayType.Float:
						Text = "0.0";
						break;
					case WatchDisplayType.Binary:
						Text = "0".PadLeft(((int)_size) * 8);
						break;
				}
			}
		}

		private void SetMaxLength()
		{
			switch (_type)
			{
				default:
					MaxLength = 8;
					break;
				case WatchDisplayType.Binary:
					MaxLength = _size switch
					{
						WatchSize.Byte => 8,
						WatchSize.Word => 16,
						_ => 8
					};
					break;
				case WatchDisplayType.Hex:
					MaxLength = _size switch
					{
						WatchSize.Byte => 2,
						WatchSize.Word => 4,
						WatchSize.DWord => 8,
						_ => 2
					};
					break;
				case WatchDisplayType.Signed:
					MaxLength = _size switch
					{
						WatchSize.Byte => 4,
						WatchSize.Word => 6,
						WatchSize.DWord => 11,
						_ => 4
					};
					break;
				case WatchDisplayType.Unsigned:
					MaxLength = _size switch
					{
						WatchSize.Byte => 3,
						WatchSize.Word => 5,
						WatchSize.DWord => 10,
						_ => 3
					};
					break;
				case WatchDisplayType.FixedPoint_12_4:
					MaxLength = 9;
					break;
				case WatchDisplayType.Float:
					MaxLength = 21;
					break;
				case WatchDisplayType.FixedPoint_20_12:
				case WatchDisplayType.FixedPoint_16_16:
					MaxLength = 64;
					break;
			}
		}

		protected override void OnKeyPress(KeyPressEventArgs e)
		{
			if (e.KeyChar == '\b' || e.KeyChar == 22 || e.KeyChar == 1 || e.KeyChar == 3)
			{
				return;
			}

			if (e.KeyChar == '.')
			{
				if (Text.Contains(".") && !SelectedText.Contains("."))
				{
					e.Handled = true;
					return;
				}
			}
			else if (e.KeyChar == '-')
			{
				if (Text.Contains("-") && !SelectedText.Contains("-"))
				{
					e.Handled = true;
					return;
				}
			}

			switch (_type)
			{
				default:
				case WatchDisplayType.Binary:
					if (!e.KeyChar.IsBinary())
					{
						e.Handled = true;
					}

					break;
				case WatchDisplayType.FixedPoint_12_4:
				case WatchDisplayType.FixedPoint_20_12:
				case WatchDisplayType.FixedPoint_16_16:
					if (!e.KeyChar.IsFixedPoint())
					{
						e.Handled = true;
					}

					break;
				case WatchDisplayType.Float:
					if (!e.KeyChar.IsFloat())
					{
						e.Handled = true;
					}

					break;
				case WatchDisplayType.Hex:
					if (!e.KeyChar.IsHex())
					{
						e.Handled = true;
					}

					break;
				case WatchDisplayType.Signed:
					if (!e.KeyChar.IsSigned())
					{
						e.Handled = true;
					}

					break;
				case WatchDisplayType.Unsigned:
					if (!e.KeyChar.IsUnsigned())
					{
						e.Handled = true;
					}

					break;
			}
		}

		protected override void OnKeyDown(KeyEventArgs e)
		{
			var text = string.IsNullOrWhiteSpace(Text) ? "0" : Text;
			if (e.KeyCode == Keys.Up)
			{
				switch (_type)
				{
					default:
					case WatchDisplayType.Signed:
						int? val = ToRawInt() ?? 0;
						if (val == MaxSignedInt)
						{
							val = MinSignedInt;
						}
						else
						{
							val++;
						}

						Text = val.ToString();
						break;
					case WatchDisplayType.Unsigned:
						var uval = (uint)(ToRawInt() ?? 0);
						if (uval == MaxUnsignedInt)
						{
							uval = 0;
						}
						else
						{
							uval++;
						}

						Text = uval.ToString();
						break;
					case WatchDisplayType.Binary:
						var bVal = (uint)(ToRawInt() ?? 0);
						if (bVal == MaxUnsignedInt)
						{
							bVal = 0;
						}
						else
						{
							bVal++;
						}

						var numBits = ((int)ByteSize) * 8;
						Text = Convert.ToString(bVal, 2).PadLeft(numBits, '0');
						break;
					case WatchDisplayType.Hex:
						var hexVal = (uint)(ToRawInt() ?? 0);
						if (hexVal == MaxUnsignedInt)
						{
							hexVal = 0;
						}
						else
						{
							hexVal++;
						}

						Text = hexVal.ToHexString(MaxLength);
						break;
					case WatchDisplayType.FixedPoint_12_4:
						var f12val = double.Parse(text);
						if (f12val > Max12_4 - _12_4_Unit)
						{
							f12val = 0;
						}
						else
						{
							f12val += _12_4_Unit;
						}

						Text = f12val.ToString();
						break;
					case WatchDisplayType.FixedPoint_20_12:
						var f24val = double.Parse(text);
						if (f24val >= Max20_12 - _20_12_Unit)
						{
							f24val = 0;
						}
						else
						{
							f24val += _20_12_Unit;
						}

						Text = f24val.ToString();
						break;
					case WatchDisplayType.FixedPoint_16_16:
						var f16val = double.Parse(text);
						if (f16val >= Max16_16 - _16_16_Unit)
						{
							f16val = 0;
						}
						else
						{
							f16val += _16_16_Unit;
						}

						Text = f16val.ToString();
						break;
					case WatchDisplayType.Float:
						var dVal = double.Parse(text);
						if (dVal > double.MaxValue - 1)
						{
							dVal = 0;
						}
						else
						{
							dVal++;
						}

						Text = dVal.ToString();
						break;
				}
			}
			else if (e.KeyCode == Keys.Down)
			{
				switch (_type)
				{
					default:
					case WatchDisplayType.Signed:
						var val = ToRawInt();
						if (!val.HasValue)
						{
							Text = "";
						}
						else if (val == MinSignedInt)
						{
							val = MaxSignedInt;
						}
						else
						{
							val--;
						}

						Text = val.ToString();
						break;
					case WatchDisplayType.Unsigned:
						var uval = (uint)(ToRawInt() ?? 0);
						if (uval == 0)
						{
							uval = MaxUnsignedInt;
						}
						else
						{
							uval--;
						}

						Text = uval.ToString();
						break;
					case WatchDisplayType.Binary:
						var bVal = (uint)(ToRawInt() ?? 0);
						if (bVal == 0)
						{
							bVal = MaxUnsignedInt;
						}
						else
						{
							bVal--;
						}

						var numBits = ((int)ByteSize) * 8;
						Text = Convert.ToString(bVal, 2).PadLeft(numBits, '0');
						break;
					case WatchDisplayType.Hex:
						var hexVal = (uint)(ToRawInt() ?? 0);
						if (hexVal == 0)
						{
							hexVal = MaxUnsignedInt;
						}
						else
						{
							hexVal--;
						}

						Text = hexVal.ToHexString(MaxLength);
						break;
					case WatchDisplayType.FixedPoint_12_4:
						var f12val = double.Parse(text);
						if (f12val < 0 + _12_4_Unit)
						{
							f12val = Max12_4;
						}
						else
						{
							f12val -= _12_4_Unit;
						}

						Text = f12val.ToString();
						break;
					case WatchDisplayType.FixedPoint_20_12:
						var f24val = double.Parse(text);
						if (f24val < 0 + _20_12_Unit)
						{
							f24val = Max20_12;
						}
						else
						{
							f24val -= _20_12_Unit;
						}

						Text = f24val.ToString();
						break;
					case WatchDisplayType.FixedPoint_16_16:
						var f16val = double.Parse(text);
						if (f16val < 0 + _16_16_Unit)
						{
							f16val = Max16_16;
						}
						else
						{
							f16val -= _16_16_Unit;
						}

						Text = f16val.ToString();
						break;
					case WatchDisplayType.Float:
						var dval = double.Parse(text);
						if (dval > double.MaxValue - 1)
						{
							dval = 0;
						}
						else
						{
							dval--;
						}

						Text = dval.ToString();
						break;
				}
			}
			else
			{
				base.OnKeyDown(e);
			}
		}

		protected override void OnTextChanged(EventArgs e)
		{
			if (string.IsNullOrWhiteSpace(Text))
			{
				ResetText();
				SelectAll();
				return;
			}

			switch (_type)
			{
				case WatchDisplayType.Signed:
					Text = Text.OnlySigned();
					break;
				case WatchDisplayType.Unsigned:
					Text = Text.OnlyUnsigned();
					break;
				case WatchDisplayType.Binary:
					Text = Text.OnlyBinary();
					break;
				case WatchDisplayType.Hex:
					Text = Text.OnlyHex();
					break;
				case WatchDisplayType.FixedPoint_12_4:
				case WatchDisplayType.FixedPoint_20_12:
				case WatchDisplayType.FixedPoint_16_16:
					Text = Text.OnlyFixedPoint();
					break;
				case WatchDisplayType.Float:
					Text = Text.OnlyFloat();
					break;
			}

			base.OnTextChanged(e);
		}

		public int? ToRawInt()
		{
			if (string.IsNullOrWhiteSpace(Text))
			{
				if (Nullable)
				{
					return null;
				}

				return 0;
			}

			switch (_type)
			{
				case WatchDisplayType.Signed:
					if (Text.IsSigned())
					{
						return Text == "-" ? 0 : int.Parse(Text);
					}

					break;
				case WatchDisplayType.Unsigned:
					if (Text.IsUnsigned())
					{
						return (int)uint.Parse(Text);
					}

					break;
				case WatchDisplayType.Binary:
					if (Text.IsBinary())
					{
						return Convert.ToInt32(Text, 2);
					}

					break;
				case WatchDisplayType.Hex:
					if (Text.IsHex())
					{
						return int.Parse(Text, NumberStyles.HexNumber);
					}

					break;
				case WatchDisplayType.FixedPoint_12_4:
					if (Text.IsFixedPoint())
					{
						return (int)(double.Parse(Text) * 16.0);
					}

					break;
				case WatchDisplayType.FixedPoint_20_12:
					if (Text.IsFixedPoint())
					{
						return (int)(double.Parse(Text) * 4096.0);
					}

					break;
				case WatchDisplayType.FixedPoint_16_16:
					if (Text.IsFixedPoint())
					{
						return (int)(double.Parse(Text) * 65536.0);
					}

					break;
				case WatchDisplayType.Float:
					if (Text.IsFloat())
					{
						if (Text == "-" || Text == ".")
						{
							return 0;
						}

						float val = float.Parse(Text);
						var bytes = BitConverter.GetBytes(val);
						return BitConverter.ToInt32(bytes, 0);
					}

					break;
			}

			return 0;
		}

		public void SetFromRawInt(int? val)
		{
			if (val.HasValue)
			{
				switch (_type)
				{
					default:
					case WatchDisplayType.Signed:
						Text = val.ToString();
						break;
					case WatchDisplayType.Unsigned:
						var uval = (uint)val.Value;
						Text = uval.ToString();
						break;
					case WatchDisplayType.Binary:
						var bVal = (uint)val.Value;
						var numBits = ((int)ByteSize) * 8;
						Text = Convert.ToString(bVal, 2).PadLeft(numBits, '0');
						break;
					case WatchDisplayType.Hex:
						Text = val.Value.ToHexString(MaxLength);
						break;
					case WatchDisplayType.FixedPoint_12_4:
						Text = $"{val.Value / 16.0:F5}";
						break;
					case WatchDisplayType.FixedPoint_20_12:
						Text = $"{val.Value / 4096.0:F5}";
						break;
					case WatchDisplayType.FixedPoint_16_16:
						Text = $"{val.Value / 65536.0:F5}";
						break;
					case WatchDisplayType.Float:
						var bytes = BitConverter.GetBytes(val.Value);
						float _float = BitConverter.ToSingle(bytes, 0);
						Text = $"{_float:F6}";
						break;
				}
			}
			else
			{
				Text = "";
			}
		}
	}
}
