using System;
using System.ComponentModel;
using System.Linq;

using BizHawk.Common;
using BizHawk.Emulation.Common;

namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
{
	/// <summary>
	/// Represents a GB add on
	/// </summary>
	public interface IPort
	{
		byte Read(IController c);

		ushort ReadAccX(IController c);

		ushort ReadAccY(IController c);

		ControllerDefinition Definition { get; }

		void SyncState(Serializer ser);

		int PortNum { get; }
	}

	[DisplayName("Gameboy Controller")]
	public class StandardControls : IPort
	{
		public StandardControls(int portNum)
		{
			PortNum = portNum;
			Definition = new ControllerDefinition
			{
				Name = "Gameboy Controller H",
				BoolButtons = BaseDefinition
				.Select(b => "P" + PortNum + " " + b)
				.ToList()
			};
		}

		public int PortNum { get; }

		public ControllerDefinition Definition { get; }

		public byte Read(IController c)
		{
			byte result = 0xFF;

			if (c.IsPressed(Definition.BoolButtons[0]))
			{
				result -= 4;
			}
			if (c.IsPressed(Definition.BoolButtons[1]))
			{
				result -= 8;
			}
			if (c.IsPressed(Definition.BoolButtons[2]))
			{
				result -= 2;
			}
			if (c.IsPressed(Definition.BoolButtons[3]))
			{
				result -= 1;
			}
			if (c.IsPressed(Definition.BoolButtons[4]))
			{
				result -= 128;
			}
			if (c.IsPressed(Definition.BoolButtons[5]))
			{
				result -= 64;
			}
			if (c.IsPressed(Definition.BoolButtons[6]))
			{
				result -= 32;
			}
			if (c.IsPressed(Definition.BoolButtons[7]))
			{
				result -= 16;
			}

			return result;
		}

		public ushort ReadAccX(IController c)
		{
			return 0;
		}

		public ushort ReadAccY(IController c)
		{
			return 0;
		}

		private static readonly string[] BaseDefinition =
		{
			"Up", "Down", "Left", "Right", "Start", "Select", "B", "A", "Power"
		};

		public void SyncState(Serializer ser)
		{
			//nothing
		}
	}

	[DisplayName("Gameboy Controller + Tilt")]
	public class StandardTilt : IPort
	{
		public StandardTilt(int portNum)
		{
			PortNum = portNum;
			Definition = new ControllerDefinition
			{
				Name = "Gameboy Controller + Tilt",
				BoolButtons = BaseDefinition.Select(b => $"P{PortNum} {b}").ToList()
			}.AddXYPair($"P{PortNum} Tilt {{0}}", AxisPairOrientation.RightAndUp, (-90).RangeTo(90), 0); //TODO verify direction against hardware
		}

		public int PortNum { get; }

		public float theta, phi, theta_prev, phi_prev, phi_prev_2; 

		public ControllerDefinition Definition { get; }

		public byte Read(IController c)
		{
			byte result = 0xFF;

			if (c.IsPressed(Definition.BoolButtons[0]))
			{
				result -= 4;
			}
			if (c.IsPressed(Definition.BoolButtons[1]))
			{
				result -= 8;
			}
			if (c.IsPressed(Definition.BoolButtons[2]))
			{
				result -= 2;
			}
			if (c.IsPressed(Definition.BoolButtons[3]))
			{
				result -= 1;
			}
			if (c.IsPressed(Definition.BoolButtons[4]))
			{
				result -= 128;
			}
			if (c.IsPressed(Definition.BoolButtons[5]))
			{
				result -= 64;
			}
			if (c.IsPressed(Definition.BoolButtons[6]))
			{
				result -= 32;
			}
			if (c.IsPressed(Definition.BoolButtons[7]))
			{
				result -= 16;
			}

			return result;
		}

		// acc x is the result of rotating around body y AFTER rotating around body x
		// therefore this control scheme gives decreasing sensitivity in X as Y rotation inscreases 
		public ushort ReadAccX(IController c)
		{
			theta_prev = theta;
			phi_prev_2 = phi_prev;
			phi_prev = phi;

			theta = (float)(c.AxisValue(Definition.Axes[1]) * Math.PI / 180.0);
			phi = (float)(c.AxisValue(Definition.Axes[0]) * Math.PI / 180.0);

			float temp = (float)(Math.Cos(theta) * Math.Sin(phi));

			// additional acceleration components are dominated by axial components due to off axis rotation.
			// They vary widely based on physical hand movements, but this roughly matches what I observe in a real GBP
			float temp2 = (float)((phi - 2 * phi_prev + phi_prev_2) * 59.7275 * 59.7275 * 0.1);

			return (ushort)(0x8370 - Math.Floor(temp * 216) - temp2);
		}

		// acc y is just the sine of the angle
		// we assume that ReadAccX is called first, which updates the the states 
		public ushort ReadAccY(IController c)
		{
			float temp = (float)Math.Sin(theta);

			// here we add in the acceleration generated by the point of rotation being far away from the accelerometer
			// this term dominates other facators due to the cartridge being far from the players hands in whatever system is being used.
			// It roughly matches what I observe in a real GBP
			float temp2 = (float)(Math.Pow((theta - theta_prev) * 59.7275, 2) * 0.15);

			return (ushort)(0x8370 - Math.Floor(temp * 216) + temp2);			
		}

		private static readonly string[] BaseDefinition =
		{
			"Up", "Down", "Left", "Right", "Start", "Select", "B", "A", "Power"
		};

		public void SyncState(Serializer ser)
		{
			// since we need rate of change of angle, need to savestate them
			ser.Sync(nameof(theta), ref theta);
			ser.Sync(nameof(phi), ref phi);
			ser.Sync(nameof(phi_prev), ref phi_prev);
		}
	}
}