using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Consoles.Sega.Saturn;
using BizHawk.Emulation.Cores.Waterbox;
using BizHawk.Emulation.DiscSystem;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using BizHawk.Common;

namespace BizHawk.Emulation.Cores.Consoles.NEC.PCFX
{
	[Core("T. S. T.", "Mednafen Team", true, true, "0.9.44.1",
		"https://mednafen.github.io/releases/", false)]
	public class Tst : WaterboxCore, IDriveLight,
		ISettable<Tst.Settings, Tst.SyncSettings>
	{
		private static readonly DiscSectorReaderPolicy _diskPolicy = new DiscSectorReaderPolicy
		{
			DeinterleavedSubcode = false
		};

		private LibTst _core;
		private Disc[] _disks;
		private DiscSectorReader[] _diskReaders;
		private LibSaturnus.CDTOCCallback _cdTocCallback;
		private LibSaturnus.CDSectorCallback _cdSectorCallback;
		private TstControllerDeck _controllerDeck;

		[CoreConstructor("PCFX")]
		public Tst(CoreComm comm, byte[] rom)
			: base(comm, new Configuration())
		{
			throw new InvalidOperationException("To load a PC-FX game, please load the CUE file and not the BIN file.");
		}

		public Tst(CoreComm comm, IEnumerable<Disc> disks, Settings settings, SyncSettings syncSettings)
			: base(comm, new Configuration
			{
				DefaultFpsNumerator = 7159091,
				DefaultFpsDenominator = 455 * 263,
				DefaultWidth = 256,
				DefaultHeight = 232,
				MaxWidth = 1024,
				MaxHeight = 480,
				MaxSamples = 2048,
				SystemId = "PCFX"
			})
		{
			var bios = comm.CoreFileProvider.GetFirmware("PCFX", "BIOS", true);
			if (bios.Length != 1024 * 1024)
				throw new InvalidOperationException("Wrong size BIOS file!");

			_disks = disks.ToArray();
			_diskReaders = disks.Select(d => new DiscSectorReader(d) { Policy = _diskPolicy }).ToArray();
			_cdTocCallback = CDTOCCallback;
			_cdSectorCallback = CDSectorCallback;
			_settings = settings ?? new Settings();
			_syncSettings = syncSettings ?? new SyncSettings();
			BufferHeight = _settings.ScanlineEnd - _settings.ScanlineStart + 1;

			_core = PreInit<LibTst>(new PeRunnerOptions
			{
				Filename = "pcfx.wbx",
				SbrkHeapSizeKB = 512,
				SealedHeapSizeKB = 2 * 1024,
				InvisibleHeapSizeKB = 4,
				PlainHeapSizeKB = 4 * 1024,
				MmapHeapSizeKB = 6 * 1024,
			});

			SetCdCallbacks();
			SetNativeSettingsBeforeInit();
			if (!_core.Init(_disks.Length, bios))
				throw new InvalidOperationException("Core rejected the CDs!");
			ClearCdCallbacks();

			PostInit();
			SetCdCallbacks();
			_controllerDeck = new TstControllerDeck(new[] { _syncSettings.Port1, _syncSettings.Port2 });
			ControllerDefinition = _controllerDeck.Definition;
			SetLayerSettings();
		}

		public override int VirtualWidth => VirtualHeight > 240 ? 586 : 293;

		protected override void LoadStateBinaryInternal(BinaryReader reader)
		{
			SetCdCallbacks();
		}

		protected override LibWaterboxCore.FrameInfo FrameAdvancePrep(IController controller, bool render, bool rendersound)
		{
			DriveLightOn = false;
			var ret = new LibTst.FrameInfo();
			var controls = _controllerDeck.GetData(controller);
			ret.Port1Buttons = controls[0];
			ret.Port2Buttons = controls[1];
			ret.ConsoleButtons = controls[2];
			return ret;
		}

		private void CDTOCCallback(int disk, [In, Out]LibSaturnus.TOC t)
		{
			Saturnus.SetupTOC(t, _disks[disk].TOC);
		}
		private void CDSectorCallback(int disk, int lba, IntPtr dest)
		{
			var buff = new byte[2448];
			_diskReaders[disk].ReadLBA_2448(lba, buff, 0);
			Marshal.Copy(buff, 0, dest, 2448);
			DriveLightOn = true;
		}

		private void SetCdCallbacks()
		{
			_core.SetCDCallbacks(_cdTocCallback, _cdSectorCallback);
		}
		private void ClearCdCallbacks()
		{
			_core.SetCDCallbacks(null, null);
		}

		public bool DriveLightEnabled => true;
		public bool DriveLightOn { get; private set; }

		#region ISettable

		public class Settings
		{
			[Description("Emulate a buggy ADPCM codec that makes some games sound better")]
			[DefaultValue(false)]
			public bool AdpcmEmulateBuggyCodec { get; set; }

			[Description("Suppress clicks on ADPCM channel resets")]
			[DefaultValue(true)]
			public bool AdpcmSuppressChannelResetClicks { get; set; }

			public enum DotClockWidths
			{
				Fastest = 256,
				Fast = 341,
				Good = 1024
			}
			[Description("Quality for high-width resolution output")]
			[DefaultValue(DotClockWidths.Good)]
			public DotClockWidths HiResEmulation { get; set; }

			[Description("Disable the hardware limit of 16 sprites per scanline")]
			[DefaultValue(false)]
			public bool DisableSpriteLimit { get; set; }
			[Description("Increase quality of some YUV output.  Can cause graphical glitches")]
			[DefaultValue(false)]
			public bool ChromaInterpolation { get; set; }

			[Description("First scanline to render")]
			[DefaultValue(4)]
			[Range(0, 8)]
			public int ScanlineStart { get; set; }

			[Description("Last scanline to render")]
			[DefaultValue(235)]
			[Range(231, 239)]
			public int ScanlineEnd { get; set; }

			[Description("Show layer BG0")]
			[DefaultValue(true)]
			public bool ShowLayerBG0 { get { return _showLayerBG0; } set { _showLayerBG0 = value; } }
			[DeepEqualsIgnore]
			private bool _showLayerBG0;

			[Description("Show layer BG1")]
			[DefaultValue(true)]
			public bool ShowLayerBG1 { get { return _showLayerBG1; } set { _showLayerBG1 = value; } }
			[DeepEqualsIgnore]
			private bool _showLayerBG1;

			[Description("Show layer BG2")]
			[DefaultValue(true)]
			public bool ShowLayerBG2 { get { return _showLayerBG2; } set { _showLayerBG2 = value; } }
			[DeepEqualsIgnore]
			private bool _showLayerBG2;

			[Description("Show layer BG3")]
			[DefaultValue(true)]
			public bool ShowLayerBG3 { get { return _showLayerBG3; } set { _showLayerBG3 = value; } }
			[DeepEqualsIgnore]
			private bool _showLayerBG3;

			[Description("Show layer VDC-A BG")]
			[DefaultValue(true)]
			public bool ShowLayerVDCABG { get { return _showLayerVDCABG; } set { _showLayerVDCABG = value; } }
			[DeepEqualsIgnore]
			private bool _showLayerVDCABG;

			[Description("Show layer VDC-A SPR")]
			[DefaultValue(true)]
			public bool ShowLayerVDCASPR { get { return _showLayerVDCASPR; } set { _showLayerVDCASPR = value; } }
			[DeepEqualsIgnore]
			private bool _showLayerVDCASPR;

			[Description("Show layer VDC-B BG")]
			[DefaultValue(true)]
			public bool ShowLayerVDCBBG { get { return _showLayerVDCBBG; } set { _showLayerVDCBBG = value; } }
			[DeepEqualsIgnore]
			private bool _showLayerVDCBBG;

			[Description("Show layer VDC-B SPR")]
			[DefaultValue(true)]
			public bool ShowLayerVDCBSPR { get { return _showLayerVDCBSPR; } set { _showLayerVDCBSPR = value; } }
			[DeepEqualsIgnore]
			private bool _showLayerVDCBSPR;

			[Description("Show layer RAINBOW")]
			[DefaultValue(true)]
			public bool ShowLayerRAINBOW { get { return _showLayerRAINBOW; } set { _showLayerRAINBOW = value; } }
			[DeepEqualsIgnore]
			private bool _showLayerRAINBOW;

			[Description("Pixel Pro.  Overrides HiResEmulation if set")]
			[DefaultValue(false)]
			public bool PixelPro { get; set; }

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

			public static bool NeedsReboot(Settings x, Settings y)
			{
				return !DeepEquality.DeepEquals(x, y);
			}

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

		public class SyncSettings
		{
			[Description("Speed of the CD-ROM drive.  Can decrease load times")]
			[DefaultValue(2)]
			[Range(2, 10)]
			public int CdSpeed { get; set; }

			public enum CpuType
			{
				Fast,
				Accurate,
				Auto
			}
			[Description("CPU emulation accuracy.  Auto chooses per game from a database")]
			[DefaultValue(CpuType.Auto)]
			public CpuType CpuEmulation { get; set; }

			[Description("Input device for the left port")]
			[DefaultValue(ControllerType.Gamepad)]
			public ControllerType Port1 { get; set; }

			[Description("Input device for the right port")]
			[DefaultValue(ControllerType.Gamepad)]
			public ControllerType Port2 { get; set; }

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

			public static bool NeedsReboot(SyncSettings x, SyncSettings y)
			{
				return !DeepEquality.DeepEquals(x, y);
			}

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

		private Settings _settings;
		private SyncSettings _syncSettings;

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

		public SyncSettings GetSyncSettings()
		{
			return _syncSettings.Clone();
		}

		public bool PutSettings(Settings o)
		{
			var ret = Settings.NeedsReboot(_settings, o);
			_settings = o;
			SetLayerSettings();
			return ret;
		}

		public bool PutSyncSettings(SyncSettings o)
		{
			var ret = SyncSettings.NeedsReboot(_syncSettings, o);
			_syncSettings = o;
			return ret;
		}

		private void SetNativeSettingsBeforeInit()
		{
			var s = new LibTst.FrontendSettings
			{
				AdpcmEmulateBuggyCodec = _settings.AdpcmEmulateBuggyCodec ? 1 : 0,
				AdpcmSuppressChannelResetClicks = _settings.AdpcmSuppressChannelResetClicks ? 1 : 0,
				HiResEmulation = (int)_settings.HiResEmulation,
				DisableSpriteLimit = _settings.DisableSpriteLimit ? 1 : 0,
				ChromaInterpolation = _settings.ChromaInterpolation ? 1 : 0,
				ScanlineStart = _settings.ScanlineStart,
				ScanlineEnd = _settings.ScanlineEnd,
				CdSpeed = _syncSettings.CdSpeed,
				CpuEmulation = (int)_syncSettings.CpuEmulation,
				Port1 = (int)_syncSettings.Port1,
				Port2 = (int)_syncSettings.Port2,
				PixelPro = _settings.PixelPro ? 1 : 0
			};
			_core.PutSettingsBeforeInit(s);
		}

		private void SetLayerSettings()
		{
			var l = LibTst.Layers.None;
			if (_settings.ShowLayerBG0)
				l |= LibTst.Layers.BG0;
			if (_settings.ShowLayerBG1)
				l |= LibTst.Layers.BG1;
			if (_settings.ShowLayerBG2)
				l |= LibTst.Layers.BG2;
			if (_settings.ShowLayerBG3)
				l |= LibTst.Layers.BG3;
			if (_settings.ShowLayerVDCABG)
				l |= LibTst.Layers.VDCA_BG;
			if (_settings.ShowLayerVDCASPR)
				l |= LibTst.Layers.VDCA_SPR;
			if (_settings.ShowLayerVDCBBG)
				l |= LibTst.Layers.VDCB_BG;
			if (_settings.ShowLayerVDCBSPR)
				l |= LibTst.Layers.VDCB_SPR;
			if (_settings.ShowLayerRAINBOW)
				l |= LibTst.Layers.RAINBOW;
			_core.EnableLayers(l);
		}

		#endregion
	}
}
