using System;
using System.Collections.Generic;

using BizHawk.Common;

namespace BizHawk.Emulation.Cores.Computers.Commodore64.Media
{
	public sealed class Disk
	{
		public const int FluxBitsPerEntry = 32;
		public const int FluxBitsPerTrack = 16000000 / 5;
		public const int FluxEntriesPerTrack = FluxBitsPerTrack / FluxBitsPerEntry;
		private readonly int[][] _tracks;
		private readonly int[] _originalMedia;
		public bool Valid;
		public bool WriteProtected;

		/// <summary>
		/// Create a blank, unformatted disk.
		/// </summary>
		public Disk(int trackCapacity)
		{
			WriteProtected = false;
			_tracks = new int[trackCapacity][];
			FillMissingTracks();
			_originalMedia = SerializeTracks(_tracks);
			Valid = true;
		}

		/// <summary>
		/// Create an expanded representation of a magnetic disk.
		/// </summary>
		/// <param name="trackData">Raw bit data.</param>
		/// <param name="trackNumbers">Track numbers for the raw bit data.</param>
		/// <param name="trackDensities">Density zones for the raw bit data.</param>
		/// <param name="trackCapacity">Total number of tracks on the media.</param>
		public Disk(IList<byte[]> trackData, IList<int> trackNumbers, IList<int> trackDensities, int trackCapacity)
		{
			WriteProtected = true;
			_tracks = new int[trackCapacity][];
			for (var i = 0; i < trackData.Count; i++)
			{
				_tracks[trackNumbers[i]] = ConvertToFluxTransitions(trackDensities[i], trackData[i], 0);
			}

			FillMissingTracks();
			Valid = true;
			_originalMedia = SerializeTracks(_tracks);
		}

		private int[] ConvertToFluxTransitions(int density, byte[] bytes, int fluxBitOffset)
		{
			var paddedLength = bytes.Length;
			switch (density)
			{
				case 3:
					paddedLength = Math.Max(bytes.Length, 7692);
					break;
				case 2:
					paddedLength = Math.Max(bytes.Length, 7142);
					break;
				case 1:
					paddedLength = Math.Max(bytes.Length, 6666);
					break;
				case 0:
					paddedLength = Math.Max(bytes.Length, 6250);
					break;
			}

			paddedLength++;
			var paddedBytes = new byte[paddedLength];
			Array.Copy(bytes, paddedBytes, bytes.Length);
			for (var i = bytes.Length; i < paddedLength; i++)
			{
				paddedBytes[i] = 0xAA;
			}
			var result = new int[FluxEntriesPerTrack];
			var lengthBits = (paddedLength * 8) - 7;
			var offsets = new List<long>();
			var remainingBits = lengthBits;

			const long bitsNum = FluxEntriesPerTrack * FluxBitsPerEntry;
			long bitsDen = lengthBits;

			for (var i = 0; i < paddedLength; i++)
			{
				var byteData = paddedBytes[i];
				for (var j = 0; j < 8; j++)
				{
					var offset = fluxBitOffset + ((i * 8 + j) * bitsNum / bitsDen);
					var byteOffset = (int)(offset / FluxBitsPerEntry);
					var bitOffset = (int)(offset % FluxBitsPerEntry);
					offsets.Add(offset);
					result[byteOffset] |= ((byteData & 0x80) != 0 ? 1 : 0) << bitOffset;
					byteData <<= 1;
					remainingBits--;
					if (remainingBits <= 0)
					{
						break;
					}
				}

				if (remainingBits <= 0)
				{
					break;
				}
			}

			return result;
		}

		private void FillMissingTracks()
		{
			// Fill half tracks (should assist with EA "fat-track" protections)
			for (var i = 1; i < _tracks.Length; i += 2)
			{
				if (_tracks[i] == null && _tracks[i - 1] != null)
				{
					_tracks[i] = new int[FluxEntriesPerTrack];
					Array.Copy(_tracks[i - 1], _tracks[i], FluxEntriesPerTrack);
				}
			}

			// Fill vacant tracks
			for (var i = 0; i < _tracks.Length; i++)
			{
				if (_tracks[i] == null)
				{
					_tracks[i] = new int[FluxEntriesPerTrack];
				}
			}
		}

		public int[] GetDataForTrack(int halftrack)
		{
			return _tracks[halftrack];
		}

		/// <summary>
		/// Combine the tracks into a single bitstream.
		/// </summary>
		private int[] SerializeTracks(int[][] tracks)
		{
			var trackCount = tracks.Length;
			var result = new int[trackCount * FluxEntriesPerTrack];
			for (var i = 0; i < trackCount; i++)
			{
				Array.Copy(tracks[i], 0, result, i * FluxEntriesPerTrack, FluxEntriesPerTrack);
			}
			return result;
		}

		/// <summary>
		/// Split a bitstream into tracks.
		/// </summary>
		private int[][] DeserializeTracks(int[] data)
		{
			var trackCount = data.Length / FluxEntriesPerTrack;
			var result = new int[trackCount][];
			for (var i = 0; i < trackCount; i++)
			{
				result[i] = new int[FluxEntriesPerTrack];
				Array.Copy(data, i * FluxEntriesPerTrack, result[i], 0, FluxEntriesPerTrack);
			}
			return result;
		}

		public void SyncState(Serializer ser)
		{
			ser.Sync(nameof(WriteProtected), ref WriteProtected);

			// Currently nothing actually writes to _tracks and so it is always the same as _originalMedia
			// So commenting out this (very slow) code for now
			// If/when disk writing is implemented, Disk.cs should implement ISaveRam as a means of file storage of the new disk state
			// And this code needs to be rethought to be reasonably performant
			//if (ser.IsReader)
			//{
			//	var mediaState = new int[_originalMedia.Length];
			//	SaveState.SyncDelta("MediaState", ser, _originalMedia, ref mediaState);
			//	_tracks = DeserializeTracks(mediaState);
			//}
			//else if (ser.IsWriter)
			//{
			//	var mediaState = SerializeTracks(_tracks);
			//	SaveState.SyncDelta("MediaState", ser, _originalMedia, ref mediaState);
			//}
		}
	}
}
