using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;

using BizHawk.Common;

namespace BizHawk.Emulation.Common
{
	[Core("NullHawk", "", false, true)]
	[ServiceNotApplicable(typeof(IStatable), typeof(ISaveRam), typeof(IDriveLight), typeof(ICodeDataLogger), typeof(IMemoryDomains),
		typeof(IDebuggable), typeof(IDisassemblable), typeof(IInputPollable), typeof(IRegionable), typeof(ITraceable), typeof(IBoardInfo))]
	public class NullEmulator : IEmulator, IVideoProvider, ISoundProvider, ISettable<NullEmulator.NullEmulatorSettings, object>
	{
		public NullEmulator(CoreComm comm, object settings)
		{
			SyncMode = SyncSoundMode.Sync;
			ServiceProvider = new BasicServiceProvider(this);
			CoreComm = comm;
			_settings = (NullEmulatorSettings)settings ?? new NullEmulatorSettings();

			var d = DateTime.Now;
			_xmas = d.Month == 12 && d.Day >= 17 && d.Day <= 27;
			if (_xmas)
			{
				_pleg = new Pleg();
			}
		}

		#region IEmulator

		public IEmulatorServiceProvider ServiceProvider { get; }

		public ControllerDefinition ControllerDefinition => NullController.Instance.Definition;

		public void FrameAdvance(IController controller, bool render, bool rendersound)
		{
			if (render == false)
			{
				return;
			}

			if (!_settings.SnowyDisplay)
			{
				if (_frameBufferClear)
				{
					return;
				}

				_frameBufferClear = true;
				Array.Clear(_frameBuffer, 0, 256 * 192);
				return;
			}

			_frameBufferClear = false;
			if (_xmas)
			{
				for (int i = 0; i < 256 * 192; i++)
				{
					byte b = (byte)_rand.Next();
					_frameBuffer[i] = Colors.ARGB(b, (byte)(255 - b), 0, 255);
				}
			}
			else
			{
				for (int i = 0; i < 256 * 192; i++)
				{
					_frameBuffer[i] = Colors.Luminosity((byte)_rand.Next());
				}
			}

			Frame++;
		}

		public int Frame { get; private set; }

		public string SystemId => "NULL";

		public bool DeterministicEmulation => true;

		public void ResetCounters()
		{
			Frame = 0;
		}

		public string BoardName => null;

		public CoreComm CoreComm { get; }

		public void Dispose()
		{
		}

		#endregion

		#region IVideoProvider

		public int[] GetVideoBuffer()
		{
			return _frameBuffer;
		}

		public int VirtualWidth => NullVideo.DefaultWidth;

		public int VirtualHeight => NullVideo.DefaultHeight;

		public int BufferWidth => NullVideo.DefaultWidth;

		public int BufferHeight => NullVideo.DefaultHeight;

		public int BackgroundColor => NullVideo.DefaultBackgroundColor;

		public int VsyncNumerator => NullVideo.DefaultVsyncNum;

		public int VsyncDenominator => NullVideo.DefaultVsyncDen;

		#endregion

		#region ISoundProvider

		public void GetSamplesSync(out short[] samples, out int nsamp)
		{
			if (SyncMode != SyncSoundMode.Sync)
			{
				throw new InvalidOperationException("Attempt to call a Sync method in async mode");
			}

			nsamp = 735;
			samples = _sampleBuffer;
			if (!_settings.SnowyDisplay)
			{
				return;
			}

			if (_xmas)
			{
				_pleg.Generate(samples);
			}
		}

		public void DiscardSamples()
		{
		}

		public void GetSamplesAsync(short[] samples)
		{
			if (SyncMode != SyncSoundMode.Async)
			{
				throw new InvalidOperationException("Attempt to call an Async method in sync mode");
			}

			if (!_settings.SnowyDisplay)
			{
				return;
			}

			if (_xmas)
			{
				_pleg.Generate(samples);
			}
		}

		public bool CanProvideAsync => true;

		public SyncSoundMode SyncMode { get; private set; }

		public void SetSyncMode(SyncSoundMode mode)
		{
			SyncMode = mode;
		}

		#endregion

		#region ISettable

		public NullEmulatorSettings GetSettings()
		{
			return _settings.Clone();
		}

		public object GetSyncSettings()
		{
			return null;
		}

		public bool PutSettings(NullEmulatorSettings o)
		{
			_settings = o;
			return false;
		}

		public bool PutSyncSettings(object o)
		{
			return false;
		}

		#endregion

		private readonly int[] _frameBuffer = new int[NullVideo.DefaultWidth * NullVideo.DefaultHeight];
		private readonly short[] _sampleBuffer = new short[735 * 2];
		private readonly Random _rand = new Random();

		private bool _frameBufferClear = true;

		private readonly bool _xmas;
		private readonly Pleg _pleg;

		private NullEmulatorSettings _settings;

		public class NullEmulatorSettings
		{
			[DefaultValue(false)]
			public bool SnowyDisplay { get; set; }

			public NullEmulatorSettings()
			{
				SettingsUtil.SetDefaultValues(this);
			}

			public NullEmulatorSettings Clone()
			{
				return (NullEmulatorSettings)MemberwiseClone();
			}
		}
	}

	#region super tone generator

	internal class Bell
	{
		private const int Sampleloop = 15360;

		// ms ima adpcm
		private const string Dataz = "[encoded data, 8084 characters]";

		private static readonly int[] AdaptationTable = { 230, 230, 230, 230, 307, 409, 512, 614, 768, 614, 512, 409, 307, 230, 230, 230 };
		private static readonly int[] AdaptCoeff1 = { 256, 512, 0, 192, 240, 460, 392 };
		private static readonly int[] AdaptCoeff2 = { 0, -256, 0, 64, 0, -208, -232 };

		private readonly byte[] _data;

		public Bell()
		{
			var gz = new System.IO.Compression.GZipStream(
				new MemoryStream(Convert.FromBase64String(Dataz), false),
				System.IO.Compression.CompressionMode.Decompress);
			var ms = new MemoryStream();
			gz.CopyTo(ms);
			_data = ms.ToArray();
			for (int i = 0; i < 3800; i++) // compenstate for sample start point
			{
				Next();
			}
		}

		private int _blockpredictor;
		private int _sample1;
		private int _sample2;
		private int _delta;

		private int _idx;
		private bool _top;

		private int _samplectr;

		public short Next()
		{
			int ret;

			if ((_idx & 0x3ff) == 0) // start block
			{
				_blockpredictor = _data[_idx] % 7;
				_delta = (short)(_data[_idx + 1] | _data[_idx + 2] << 8);
				_sample1 = (short)(_data[_idx + 3] | _data[_idx + 4] << 8);
				_sample2 = (short)(_data[_idx + 5] | _data[_idx + 6] << 8);

				ret = _sample2;
				_idx++;
			}
			else if ((_idx & 0x3ff) == 1)
			{
				ret = _sample1;
				_top = true;
				_idx += 6;
			}
			else
			{
				int nibble = _data[_idx];
				if (_top)
				{
					nibble >>= 4;
				}
				else
				{
					_idx++;
				}

				_top ^= true;
				nibble <<= 28;
				nibble >>= 28;

				int predictor = (_sample1 * AdaptCoeff1[_blockpredictor]) + (_sample2 * AdaptCoeff2[_blockpredictor]);
				predictor >>= 8;
				predictor += nibble * _delta;
				if (predictor >= 32767)
				{
					predictor = 32767;
				}

				if (predictor <= -32768)
				{
					predictor = -32768;
				}

				ret = predictor;
				_sample2 = _sample1;
				_sample1 = predictor;
				_delta = AdaptationTable[nibble & 15] * _delta;
				_delta >>= 8;
				if (_delta < 16)
				{
					_delta = 16;
				}
			}

			_samplectr++;
			if (_samplectr == Sampleloop)
			{
				_samplectr = 0;
				_idx = 0;
			}

			return (short)ret;
		}
	}

	internal class Pleg
	{
		private const string Data = "H4sICI/2sVICAG91dDMudHh0AOxazdLbIAy8d6bvgkFImFsvufb936Yt3YyKvjBY5UvS6XDSxOZndyULy9H3ylLD1y8/baxHs/Lb5rNG2IT7zVKq9Msmrmf7Tb/st3qcP4ff7rdhb7itw04eXrVzsYWOTuXTt7yzl/OXvYHtDWwN+0cQi0IcqzJnxtchy9lDbo5rVODAAJvbdXWk1PiQooBiMBQPnxcOnYbhfkoCSgGUMmLxbgsoCSgdoCSgFEApwxZQArZ0uryWTp227DUBxVzDpbXLNUhlAVIGJELsZ6hb+kzACdePGqFqxPiE8QnjEualCcUZtb+mRKAUP0tlfyxHQAiIZUEsJ6gZYVXtTlVOiGWBmhk29qoS+zIQ6zQvJZ3rUHFtSwm9I++q5WJUS1At90mNAywhA/CqausZIPaPG/Jtgwhq6ug3qU5GdZMRMg+OmNR7IxfjjQwbDLXD5Q09Yta9QcfqKQfkz4Aw3fptrP0xNVfsCVu++j1S55KPJem01Yi2Bw/R27N2yxfj9znNI9TnESo1dikyT7J68aledNqi6vO1yjUI5RkQplu/mTWRf8u7LVTzZeXaaBRNeUxDTozimi8HRhuNqM/XJZOoiK5IeLJFOF5bEV3XSBGxeHiwjDSbaTXRBkhmuBUBU83T9IiK/wEPUmQOf3RIZxqxI2YVEQfDy7C3VZzJuWTqDuTkDzmW9PUT49KfXHIAlzD0s+qk6CJWx2ptFdzt9mqWsuYF6KT6aBoRAmWGK3MPMfEIkoHg2JIRPfajC39U1/K2TCeQ3SrqHi4V+YSK8VUq2hJoriKDd3So+NJYtBTUnvV4jaqq1omtCVYGsdi9RVmIyDdzqJoPNLdZ6O0q5MhzKh8LUAIFGQSIraFFA8VSg0QOagAJ+5xY1xpaBrGel2I9j2Nd63Kiv8u7tBDb5Mu7xaiYH6uovAcq0ttV5KIxvq6iMxb/HxV7CmpLPV6i6vhrGZdRHp5Us/SEPEwmD5eaXQEzycN5kIfZ5GHjDS7LediftAaxH/DN0r5riPWOLXld3xiI/unqWhgqnbCHieGzU8v9/YJK2wWrSqxHA0404bv+7yjpy1G7HwGBFAoiOIJw9PsABHVVHhBc+G8UJyAAYwv1lJASaZZAiPFbzCN6Pq7zKPq+pUWdtuy7oo9qp2YCNe59xGwe0RmWco1CWaDAfeKUA95KfXmA6+qlWKOpwieUZlTW/0NNSqH9DoAcAfmosUuYx2d5wf+MpP4ZYYbqAdBpoP5x73ExrRFHXwuKpSa+Z0R0mo+aFqsygKRrj9SerYqrZu1V3CRuqRbougPdId0qxLlfR6Psgam9PBxhT+wd+71zcKmeg05bVBWQboBkIF7Zq8xWxdXJ2iuZfILTSuil/SxIqSxDu+bX+RHOYjIxwUZTQIgeKoOuQ2Ac993tbsTdjbi7EXc34u5G3N2IuxtxdyPubsTdjbi7EXc34u5G3N2IuxtxdyPubsTdjbi7EXc34o927dAGAACEgeB27D8SEoVBleRmqGg+ORqRRqQRaUQakUakEWlEGjG1rmlEGpFGpBFpRBqRRqQRaUQakUakEWlEGpFGpBFpRBqRRqQRaUQakUakEWlEGpFGpBFpRBqRRqQRaUQakUb86OhoRBqRRqQRk+qaRqQRaUQakUakEWlEGpFGpBFvGnFXiHMetSzUwqZz46p5AAA=";
		private readonly List<SinMan> _sinMen = new List<SinMan>();
		private readonly List<string> _lines = new List<string>();
		private readonly Bell _bell = new Bell();

		private int _lineIdx;
		private int _deadtime;

		public Pleg()
		{
			var gz = new System.IO.Compression.GZipStream(
				new MemoryStream(Convert.FromBase64String(Data), false),
				System.IO.Compression.CompressionMode.Decompress);
			var tr = new StreamReader(gz);
			string line;
			while ((line = tr.ReadLine()) != null)
			{
				_lines.Add(line);
			}
		}

		private void Off(int c, int n)
		{
			foreach (var s in _sinMen)
			{
				if (s.C == c && s.N == n && !s.Fading)
				{
					s.Fading = true;
				}
			}
		}

		private void On(int c, int n)
		{
			if (c == 9)
			{
				return;
			}

			var s = new SinMan(1500, n) { C = c, N = n };
			_sinMen.Add(s);
		}

		private short Next()
		{
			int ret = 0;
			for (int i = 0; i < _sinMen.Count; i++)
			{
				var s = _sinMen[i];
				if (s.Done)
				{
					_sinMen.RemoveAt(i);
					i--;
				}
				else
				{
					ret += s.Next();
				}
			}

			if (ret > 32767)
			{
				ret = 32767;
			}

			if (ret < -32767)
			{
				ret = -32767;
			}

			return (short)ret;
		}

		private string FetchNext()
		{
			string ret = _lines[_lineIdx];
			_lineIdx++;
			if (_lineIdx == _lines.Count)
			{
				_lineIdx = 0;
			}

			return ret;
		}

		public void Generate(short[] dest)
		{
			int idx = 0;
			while (idx < dest.Length)
			{
				if (_deadtime > 0)
				{
					short n = Next();
					n += _bell.Next();
					dest[idx++] = n;
					dest[idx++] = n;
					_deadtime--;
				}
				else
				{
					string[] s = FetchNext().Split(':');
					char c = s[0][0];
					if (c == 'A')
					{
						_deadtime = int.Parse(s[1]) * 40;
					}
					else if (c == 'O')
					{
						On(int.Parse(s[2]), int.Parse(s[1]));
					}
					else if (c == 'F')
					{
						Off(int.Parse(s[2]), int.Parse(s[1]));
					}
				}
			}
		}
	}

	internal class SinMan
	{
		private readonly double _freq;

		private double _theta;
		private double _amp;

		public SinMan(int amp, int note)
		{
			_amp = amp;
			_freq = GetFreq(note);
		}

		public int C { get; set; }
		public int N { get; set; }

		public bool Fading { get; set; }

		public bool Done => _amp < 2.0;

		private static double GetFreq(int note)
		{
			return Math.Pow(2.0, note / 12.0) * 13.0;
		}

		public short Next()
		{
			short result = (short)(Math.Sin(_theta) * _amp);
			_theta += _freq * Math.PI / 22050.0;
			if (_theta >= Math.PI * 2.0)
			{
				_theta -= Math.PI * 2.0;
			}

			if (Fading)
			{
				_amp *= 0.87;
			}

			return result;
		}
	}

	#endregion
}
