using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using BizHawk.Common;

namespace BizHawk.Emulation.Common.Components
{
	public sealed class SN76489 : ISoundProvider
	{
		public sealed class Channel
		{
			public ushort Frequency;
			public byte Volume;
			public short[] Wave;
			public bool Noise;
			public byte NoiseType;
			public float WaveOffset;
			public bool Left = true;
			public bool Right = true;

			const int SampleRate = 44100;
			private static readonly byte[] LogScale = { 0, 10, 13, 16, 20, 26, 32, 40, 51, 64, 81, 102, 128, 161, 203, 255 };

			public void Mix(short[] samples, int start, int len, int maxVolume)
			{
				if (Volume == 0) return;

				float adjustedWaveLengthInSamples = SampleRate / (Noise ? (Frequency / (float)Wave.Length) : Frequency);
				float moveThroughWaveRate = Wave.Length / adjustedWaveLengthInSamples;

				int end = start + len;
				for (int i = start; i < end; )
				{
					short value = Wave[(int)WaveOffset];

					samples[i++] += (short)(Left ? (value / 4 * LogScale[Volume] / 0xFF * maxVolume / short.MaxValue) : 0);
					samples[i++] += (short)(Right ? (value / 4 * LogScale[Volume] / 0xFF * maxVolume / short.MaxValue) : 0);
					WaveOffset += moveThroughWaveRate;
					if (WaveOffset >= Wave.Length)
						WaveOffset %= Wave.Length;
				}
			}
		}

		public Channel[] Channels = new Channel[4];
		public byte PsgLatch;

		private readonly Queue<QueuedCommand> commands = new Queue<QueuedCommand>(256);
		int frameStartTime, frameStopTime;

		const int PsgBase = 111861;

		public SN76489()
		{
			MaxVolume = short.MaxValue * 2 / 3;
			Waves.InitWaves();
			for (int i = 0; i < 4; i++)
			{
				Channels[i] = new Channel();
				switch (i)
				{
					case 0:
					case 1:
					case 2:
						Channels[i].Wave = Waves.ImperfectSquareWave;
						break;
					case 3:
						Channels[i].Wave = Waves.NoiseWave;
						Channels[i].Noise = true;
						break;
				}
			}
		}

		public void Reset()
		{
			PsgLatch = 0;
			foreach (var channel in Channels)
			{
				channel.Frequency = 0;
				channel.Volume = 0;
				channel.NoiseType = 0;
				channel.WaveOffset = 0f;
			}
		}

		public void BeginFrame(int cycles)
		{
			while (commands.Count > 0)
			{
				var cmd = commands.Dequeue();
				WritePsgDataImmediate(cmd.Value);
			}
			frameStartTime = cycles;
		}

		public void EndFrame(int cycles)
		{
			frameStopTime = cycles;
		}

		public void WritePsgData(byte value, int cycles)
		{
			commands.Enqueue(new QueuedCommand { Value = value, Time = cycles - frameStartTime });
		}

		void UpdateNoiseType(int value)
		{
			Channels[3].NoiseType = (byte)(value & 0x07);
			switch (Channels[3].NoiseType & 3)
			{
				case 0: Channels[3].Frequency = PsgBase / 16; break;
				case 1: Channels[3].Frequency = PsgBase / 32; break;
				case 2: Channels[3].Frequency = PsgBase / 64; break;
				case 3: Channels[3].Frequency = Channels[2].Frequency; break;
			}
			var newWave = (value & 4) == 0 ? Waves.PeriodicWave16 : Waves.NoiseWave;
			if (newWave != Channels[3].Wave)
			{
				Channels[3].Wave = newWave;
				Channels[3].WaveOffset = 0f;
			}
		}

		void WritePsgDataImmediate(byte value)
		{
			switch (value & 0xF0)
			{
				case 0x80:
				case 0xA0:
				case 0xC0:
					PsgLatch = value;
					break;
				case 0xE0:
					PsgLatch = value;
					UpdateNoiseType(value);
					break;
				case 0x90:
					Channels[0].Volume = (byte)(~value & 15);
					PsgLatch = value;
					break;
				case 0xB0:
					Channels[1].Volume = (byte)(~value & 15);
					PsgLatch = value;
					break;
				case 0xD0:
					Channels[2].Volume = (byte)(~value & 15);
					PsgLatch = value;
					break;
				case 0xF0:
					Channels[3].Volume = (byte)(~value & 15);
					PsgLatch = value;
					break;
				default:
					byte channel = (byte)((PsgLatch & 0x60) >> 5);
					if ((PsgLatch & 16) == 0) // Tone latched
					{
						int f = PsgBase / (((value & 0x03F) * 16) + (PsgLatch & 0x0F) + 1);
						if (f > 15000)
							f = 0; // upper bound of playable frequency
						Channels[channel].Frequency = (ushort)f;
						if ((Channels[3].NoiseType & 3) == 3 && channel == 2)
							Channels[3].Frequency = (ushort)f;
					}
					else
					{ // volume latched
						Channels[channel].Volume = (byte)(~value & 15);
					}
					break;
			}
		}

		byte stereoPanning = 0xFF;
		public byte StereoPanning
		{
			get
			{
				byte value = 0;
				if (Channels[0].Left)  value |= 0x10;
				if (Channels[0].Right) value |= 0x01;
				if (Channels[1].Left)  value |= 0x20;
				if (Channels[1].Right) value |= 0x02;
				if (Channels[2].Left)  value |= 0x40;
				if (Channels[2].Right) value |= 0x04;
				if (Channels[3].Left)  value |= 0x80;
				if (Channels[3].Right) value |= 0x08;
				return value;
			}
			set
			{
				Channels[0].Left  = (value & 0x10) != 0;
				Channels[0].Right = (value & 0x01) != 0;
				Channels[1].Left  = (value & 0x20) != 0;
				Channels[1].Right = (value & 0x02) != 0;
				Channels[2].Left  = (value & 0x40) != 0;
				Channels[2].Right = (value & 0x04) != 0;
				Channels[3].Left  = (value & 0x80) != 0;
				Channels[3].Right = (value & 0x08) != 0;
				stereoPanning = value;
			}
		}

		public void SyncState(Serializer ser)
		{
			ser.BeginSection("PSG");
			ser.Sync("Volume0", ref Channels[0].Volume);
			ser.Sync("Volume1", ref Channels[1].Volume);
			ser.Sync("Volume2", ref Channels[2].Volume);
			ser.Sync("Volume3", ref Channels[3].Volume);
			ser.Sync("Freq0", ref Channels[0].Frequency);
			ser.Sync("Freq1", ref Channels[1].Frequency);
			ser.Sync("Freq2", ref Channels[2].Frequency);
			ser.Sync("Freq3", ref Channels[3].Frequency);
			ser.Sync("NoiseType", ref Channels[3].NoiseType);
			ser.Sync("PsgLatch", ref PsgLatch);
			ser.Sync("Panning", ref stereoPanning);
			ser.EndSection();

			if (ser.IsReader)
			{
				StereoPanning = stereoPanning;
				UpdateNoiseType(Channels[3].NoiseType);
			}
		}

		public int MaxVolume { get; set; }
		public void DiscardSamples() { commands.Clear(); }
		public void GetSamples(short[] samples)
		{
			int elapsedCycles = frameStopTime - frameStartTime;
			if (elapsedCycles == 0)
				elapsedCycles = 1; // hey it's better than diving by zero

			int start = 0;
			while (commands.Count > 0)
			{
				var cmd = commands.Dequeue();
				int pos = ((cmd.Time * samples.Length) / elapsedCycles) & ~1;
				GetSamplesImmediate(samples, start, pos - start);
				start = pos;
				WritePsgDataImmediate(cmd.Value);
			}
			GetSamplesImmediate(samples, start, samples.Length - start);
		}

		public void GetSamplesImmediate(short[] samples, int start, int len)
		{
			for (int i = 0; i < 4; i++)
				Channels[i].Mix(samples, start, len, MaxVolume);
		}

		class QueuedCommand
		{
			public byte Value;
			public int Time;
		}
	}
}