using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BizHawk.Emulation.Cores.Computers.AmstradCPC
{
	/// <summary>
	/// Responsible for TZX format serialization
	/// </summary>
	public class CdtConverter : MediaConverter
	{
		/// <summary>
		/// The type of serializer
		/// </summary>
		private readonly MediaConverterType _formatType = MediaConverterType.CDT;
		public override MediaConverterType FormatType => _formatType;

		/// <summary>
		/// Signs whether this class can be used to read the data format
		/// </summary>
		public override bool IsReader => true;

		/// <summary>
		/// Signs whether this class can be used to write the data format
		/// </summary>
		public override bool IsWriter => false;

		/// <summary>
		/// Working list of generated tape data blocks
		/// </summary>
		private List<TapeDataBlock> _blocks = new List<TapeDataBlock>();

		/// <summary>
		/// Position counter
		/// </summary>
		private int _position = 0;

		/// <summary>
		/// Object to keep track of loops - this assumes there is only one loop at a time
		/// </summary>
		private readonly List<KeyValuePair<int, int>> _loopCounter = new List<KeyValuePair<int, int>>();

		private readonly DatacorderDevice _datacorder;

		public CdtConverter(DatacorderDevice _tapeDevice)
		{
			_datacorder = _tapeDevice;
		}

		/// <summary>
		/// CDT format is essentially exactly the same as the TZX format
		/// However all timings are based on spectrum timings (3.5Mhz)
		/// so need to be adjusted for the CPC (4Mhz)
		/// </summary>
		private TapeDataBlock ConvertClock(TapeDataBlock db)
		{
			var tb = new TapeDataBlock
			{
				BlockDescription = db.BlockDescription,
				BlockID = db.BlockID,
				Command = db.Command,
				DataPeriods = new List<int>(),
				InitialPulseLevel = db.InitialPulseLevel,
				MetaData = db.MetaData,
				PauseInMS = db.PauseInMS
			};

			double multiplier = 8.0 / 7.0;
			//double cycleScale = ((40 << 16) / 35);
			double origPeriods = db.DataPeriods.Count;

			for (int i = 0; i < origPeriods; i++)
			{
				int orig = db.DataPeriods[i];
				int np = (int)((double)orig * multiplier);
				int nnp = ClockAdjust(orig);
				tb.DataPeriods.Add(np);
			}

			return tb;
		}

		private int ClockAdjust(int val)
		{
			int cycleScale = ((40 << 16) / 35);
			int res = (val * cycleScale) >> 16;
			return res;
		}

		private int Scale => ((40 << 16) / 35);

		private int Adjust(int val)
		{
			return (int)((val * CLOCK_MULTIPLIER));
		}

		private const double CLOCK_MULTIPLIER = 1.142857;

		/// <summary>
		/// Returns TRUE if tzx header is detected
		/// </summary>
		public override bool CheckType(byte[] data)
		{
			/*
            // TZX Header
            length: 10 bytes
            Offset  Value       Type        Description
            0x00    "ZXTape!"   ASCII[7]    TZX signature
            0x07    0x1A        BYTE        End of text file marker
            0x08    1           BYTE        TZX major revision number
            0x09    20          BYTE        TZX minor revision number
            */

			// check whether this is a valid tzx format file by looking at the identifier in the header
			// (first 7 bytes of the file)
			string ident = Encoding.ASCII.GetString(data, 0, 7);
			// and 'end of text' marker
			byte eotm = data[7];

			// version info
			int majorVer = data[8];
			int minorVer = data[9];

			if (ident != "ZXTape!" || eotm != 0x1A)
			{
				// this is not a valid TZX format file
				return false;
			}
			else
			{
				return true;
			}
		}

		/// <summary>
		/// DeSerialization method
		/// </summary>
		public override void Read(byte[] data)
		{
			// clear existing tape blocks
			_datacorder.DataBlocks.Clear();

			/*
						// TZX Header
						length: 10 bytes
						Offset  Value       Type        Description
						0x00    "ZXTape!"   ASCII[7]    TZX signature
						0x07    0x1A        BYTE        End of text file marker
						0x08    1           BYTE        TZX major revision number
						0x09    20          BYTE        TZX minor revision number
			*/

			// check whether this is a valid tzx format file by looking at the identifier in the header
			// (first 7 bytes of the file)
			string ident = Encoding.ASCII.GetString(data, 0, 7);
			// and 'end of text' marker
			byte eotm = data[7];

			// version info
			int majorVer = data[8];
			int minorVer = data[9];

			if (ident != "ZXTape!" || eotm != 0x1A)
			{
				// this is not a valid TZX format file
				throw new Exception(this.GetType() +
					"This is not a valid TZX format file");
			}

			// iterate through each block
			_position = 10;
			while (_position < data.Length)
			{
				// block ID is the first byte in a new block
				int ID = data[_position++];

				// process the data
				ProcessBlock(data, ID);
			}

			/*
            // convert for Amstrad CPC
            List<TapeDataBlock> newBlocks = new List<TapeDataBlock>();
            for (int i = 0; i < _datacorder.DataBlocks.Count; i++)
            {
                newBlocks.Add(ConvertClock(_datacorder.DataBlocks[i]));
            }

            _datacorder.DataBlocks.Clear();
            _datacorder.DataBlocks.AddRange(newBlocks);
            */
		}

		/// <summary>
		/// Processes a TZX block
		/// </summary>
		private void ProcessBlock(byte[] data, int id)
		{
			// process based on detected block ID
			switch (id)
			{
				// ID 10 - Standard Speed Data Block
				case 0x10:
					ProcessBlockID10(data);
					break;
				// ID 11 - Turbo Speed Data Block
				case 0x11:
					ProcessBlockID11(data);
					break;
				// ID 12 - Pure Tone
				case 0x12:
					ProcessBlockID12(data);
					break;
				// ID 13 - Pulse sequence
				case 0x13:
					ProcessBlockID13(data);
					break;
				// ID 14 - Pure Data Block
				case 0x14:
					ProcessBlockID14(data);
					break;
				// ID 15 - Direct Recording
				case 0x15:
					ProcessBlockID15(data);
					break;
				// ID 18 - CSW Recording
				case 0x18:
					ProcessBlockID18(data);
					break;
				// ID 19 - Generalized Data Block
				case 0x19:
					ProcessBlockID19(data);
					break;
				// ID 20 - Pause (silence) or 'Stop the Tape' command
				case 0x20:
					ProcessBlockID20(data);
					break;
				// ID 21 - Group start
				case 0x21:
					ProcessBlockID21(data);
					break;
				// ID 22 - Group end
				case 0x22:
					ProcessBlockID22(data);
					break;
				// ID 23 - Jump to block
				case 0x23:
					ProcessBlockID23(data);
					break;
				// ID 24 - Loop start
				case 0x24:
					ProcessBlockID24(data);
					break;
				// ID 25 - Loop end
				case 0x25:
					ProcessBlockID25(data);
					break;
				// ID 26 - Call sequence
				case 0x26:
					ProcessBlockID26(data);
					break;
				// ID 27 - Return from sequence
				case 0x27:
					ProcessBlockID27(data);
					break;
				// ID 28 - Select block
				case 0x28:
					ProcessBlockID28(data);
					break;
				// ID 2A - Stop the tape if in 48K mode
				case 0x2A:
					ProcessBlockID2A(data);
					break;
				// ID 2B - Set signal level
				case 0x2B:
					ProcessBlockID2B(data);
					break;
				// ID 30 - Text description
				case 0x30:
					ProcessBlockID30(data);
					break;
				// ID 31 - Message block
				case 0x31:
					ProcessBlockID31(data);
					break;
				// ID 32 - Archive info
				case 0x32:
					ProcessBlockID32(data);
					break;
				// ID 33 - Hardware type
				case 0x33:
					ProcessBlockID33(data);
					break;
				// ID 35 - Custom info block
				case 0x35:
					ProcessBlockID35(data);
					break;
				// ID 5A - "Glue" block
				case 0x5A:
					ProcessBlockID5A(data);
					break;

				// ID 16 - (deprecated) C64 ROM Type Data Block
				case 0x16:
					ProcessBlockID16(data);
					break;
				// ID 17 - (deprecated) C64 Turbo Tape Data Block
				case 0x17:
					ProcessBlockID17(data);
					break;
				// ID 34 - (deprecated) Emulation info
				case 0x34:
					ProcessBlockID34(data);
					break;
				// ID 40 - (deprecated) Snapshot block
				case 0x40:
					ProcessBlockID40(data);
					break;

				default:
					ProcessUnidentifiedBlock(data);
					break;
			}
		}

		/*              length: [02,03]+04
				Offset	    Value	Type	    Description
				0x00	    -	    WORD	    Pause after this block (ms.) {1000}
				0x02	    N	    WORD	    Length of data that follow
				0x04	    -	    BYTE[N]	    Data as in .TAP files                   

				This block must be replayed with the standard Spectrum ROM timing values - see the values in 
				curly brackets in block ID 11. The pilot tone consists in 8063 pulses if the first data byte 
				(flag byte) is < 128, 3223 otherwise. This block can be used for the ROM loading routines AND 
				for custom loading routines that use the same timings as ROM ones do. */
		private void ProcessBlockID10(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x10,
				BlockDescription = BlockType.Standard_Speed_Data_Block,
				DataPeriods = new List<int>()
			};

			int pauseLen = GetWordValue(data, _position);
			if (pauseLen == 0)
				pauseLen = 1000;

			t.PauseInMS = pauseLen;

			int blockLen = GetWordValue(data, _position + 2);

			_position += 4;

			byte[] tmp = new byte[blockLen];
			tmp = data.Skip(_position).Take(blockLen).ToArray();

			var t2 = DecodeDataBlock(t, tmp, DataBlockType.Standard, pauseLen);

			// add the block
			_datacorder.DataBlocks.Add(t2);

			// advance the position to the next block
			_position += blockLen;

			// generate PAUSE block
			CreatePauseBlock(_datacorder.DataBlocks.Last());
		}

		/*              length: [0F,10,11]+12
				Offset	Value	Type	Description
				0x00	-	    WORD	Length of PILOT pulse {2168}
				0x02	-	    WORD	Length of SYNC first pulse {667}
				0x04	-	    WORD	Length of SYNC second pulse {735}
				0x06	-	    WORD	Length of ZERO bit pulse {855}
				0x08	-	    WORD	Length of ONE bit pulse {1710}
				0x0A	-	    WORD	Length of PILOT tone (number of pulses) {8063 header (flag<128), 3223 data (flag>=128)}
				0x0C	-	    BYTE	Used bits in the last byte (other bits should be 0) {8}
										(e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00, 
										where MSb is the leftmost bit, LSb is the rightmost bit)
				0x0D	-	    WORD	Pause after this block (ms.) {1000}
				0x0F	N	    BYTE[3]	Length of data that follow
				0x12	-	    BYTE[N]	Data as in .TAP files                  

				This block is very similar to the normal TAP block but with some additional info on the timings and other important 
				differences. The same tape encoding is used as for the standard speed data block. If a block should use some non-standard 
				sync or pilot tones (i.e. all sorts of protection schemes) then use the next three blocks to describe it.*/
		private void ProcessBlockID11(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x11,
				BlockDescription = BlockType.Turbo_Speed_Data_Block,
				DataPeriods = new List<int>()
			};

			int pilotPL = GetWordValue(data, _position);
			int sync1P = GetWordValue(data, _position + 2);
			int sync2P = GetWordValue(data, _position + 4);
			int bit0P = GetWordValue(data, _position + 6);
			int bit1P = GetWordValue(data, _position + 8);
			int pilotTL = GetWordValue(data, _position + 10);
			int bitinbyte = data[_position + 12];
			int pause = GetWordValue(data, _position + 13);


			int blockLen = 0xFFFFFF & GetInt32(data, _position + 0x0F);

			byte[] bLenArr = data.Skip(_position + 0x0F).Take(3).ToArray();

			_position += 0x12;

			byte[] tmp = new byte[blockLen];
			tmp = data.Skip(_position).Take(blockLen).ToArray();

			var t2 = DecodeDataBlock(t, tmp, DataBlockType.Turbo, pause, pilotTL, pilotPL, sync1P, sync2P, bit0P, bit1P, bitinbyte);

			t.PauseInMS = pause;

			// add the block
			_datacorder.DataBlocks.Add(t2);

			// advance the position to the next block
			_position += blockLen;

			// generate PAUSE block
			CreatePauseBlock(_datacorder.DataBlocks.Last());
		}

		/*              length: 04
				Offset	Value	Type	Description
				0x00	-	    WORD	Length of one pulse in T-states
				0x02	-	    WORD	Number of pulses                                

				This will produce a tone which is basically the same as the pilot tone in the ID 10, ID 11 blocks. You can define how 
				long the pulse is and how many pulses are in the tone. */
		private void ProcessBlockID12(byte[] data)
		{
			int blockLen = 4;

			var t = new TapeDataBlock
			{
				BlockID = 0x12,
				BlockDescription = BlockType.Pure_Tone,
				DataPeriods = new List<int>(), PauseInMS = 0
			};

			// get values
			int pulseLength = GetWordValue(data, _position);
			int pulseCount = GetWordValue(data, _position + 2);

			t.AddMetaData(BlockDescriptorTitle.Pulse_Length, pulseLength + " T-States");
			t.AddMetaData(BlockDescriptorTitle.Pulse_Count, pulseCount.ToString());

			// build period information
			for (int p = 0; p < pulseCount; p++)
			{
				t.DataPeriods.Add(pulseLength);
			}

			// add the block
			_datacorder.DataBlocks.Add(t);

			// advance the position to the next block
			_position += blockLen;
		}

		/*              length: [00]*02+01
				Offset	Value	Type	Description
				0x00	N	    BYTE	Number of pulses
				0x01	-	    WORD[N]	Pulses' lengths                               

				This will produce N pulses, each having its own timing. Up to 255 pulses can be stored in this block; this is useful for non-standard 
				sync tones used by some protection schemes. */
		private void ProcessBlockID13(byte[] data)
		{
			TapeDataBlock t = new TapeDataBlock
			{
				BlockID = 0x13,
				BlockDescription = BlockType.Pulse_Sequence,
				DataPeriods = new List<int>(),
				PauseInMS = 0
			};

			// get pulse count
			int pulseCount = data[_position];
			t.AddMetaData(BlockDescriptorTitle.Pulse_Count, pulseCount.ToString());
			_position++;

			// build period information
			for (int p = 0; p < pulseCount; p++, _position += 2)
			{
				// get pulse length
				int pulseLength = GetWordValue(data, _position);
				t.AddMetaData(BlockDescriptorTitle.Needs_Parsing, "Pulse " + p + " Length\t" + pulseLength + " T-States");
				t.DataPeriods.Add(pulseLength);
			}

			// add the block
			_datacorder.DataBlocks.Add(t);
		}

		/*              length: [07,08,09]+0A
				Offset	Value	Type	Description
				0x00	-	    WORD	Length of ZERO bit pulse
				0x02	-	    WORD	Length of ONE bit pulse
				0x04	-	    BYTE	Used bits in last byte (other bits should be 0)
										(e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00, 
										where MSb is the leftmost bit, LSb is the rightmost bit)
				0x05	-	    WORD	Pause after this block (ms.)
				0x07	N	    BYTE[3]	Length of data that follow
				0x0A	-	    BYTE[N]	Data as in .TAP files                             

				This is the same as in the turbo loading data block, except that it has no pilot or sync pulses. */
		private void ProcessBlockID14(byte[] data)
		{
			TapeDataBlock t = new TapeDataBlock
			{
				BlockID = 0x14,
				BlockDescription = BlockType.Pure_Data_Block,
				DataPeriods = new List<int>()
			};

			int pilotPL = 0;
			int sync1P = 0;
			int sync2P = 0;
			int bit0P = GetWordValue(data, _position + 0);
			int bit1P = GetWordValue(data, _position + 2);
			int pilotTL = 0;
			int bitinbyte = data[_position + 4];
			int pause = GetWordValue(data, _position + 5);

			int blockLen = 0xFFFFFF & GetInt32(data, _position + 0x07);

			_position += 0x0A;

			byte[] tmp = new byte[blockLen];
			tmp = data.Skip(_position).Take(blockLen).ToArray();

			var t2 = DecodeDataBlock(t, tmp, DataBlockType.Pure, pause, pilotTL, pilotPL, sync1P, sync2P, bit0P, bit1P, bitinbyte);

			t.PauseInMS = pause;

			// add the block
			_datacorder.DataBlocks.Add(t2);

			// advance the position to the next block
			_position += blockLen;

			// generate PAUSE block
			CreatePauseBlock(_datacorder.DataBlocks.Last());
		}

		/*              length: [05,06,07]+08
				Offset	Value	Type	Description
				0x00	-	    WORD	Number of T-states per sample (bit of data)
				0x02	-	    WORD	Pause after this block in milliseconds (ms.)
				0x04	-	    BYTE	Used bits (samples) in last byte of data (1-8)
										(e.g. if this is 2, only first two samples of the last byte will be played)
				0x05	N	    BYTE[3]	Length of samples' data
				0x08	-	    BYTE[N]	Samples data. Each bit represents a state on the EAR port (i.e. one sample).
										MSb is played first.                            

				This block is used for tapes which have some parts in a format such that the turbo loader block cannot be used. 
				This is not like a VOC file, since the information is much more compact. Each sample value is represented by one bit only 
				(0 for low, 1 for high) which means that the block will be at most 1/8 the size of the equivalent VOC.
				The preferred sampling frequencies are 22050 or 44100 Hz (158 or 79 T-states/sample). 
				Please, if you can, don't use other sampling frequencies.
				Please use this block only if you cannot use any other block. */
		private void ProcessBlockID15(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x15,
				BlockDescription = BlockType.Direct_Recording,
				DataPeriods = new List<int>()
			};

			// get values
			int samLen = GetInt32(data, _position + 5);
			int samSize = 0xFFFFFF & samLen;

			int tStatesPerSample = GetWordValue(data, _position);
			int pauseAfterBlock = GetWordValue(data, _position + 2);
			int usedBitsInLastByte = data[_position + 4];

			// skip to samples data
			_position += 8;

			int pulseLength = 0;
			int pulseCount = 0;

			// ascertain the pulse count
			for (int i = 0; i < samSize; i++)
			{
				for (int p = 0x80; p != 0; p >>= 1)
				{
					if (((data[_position + i] ^ pulseLength) & p) != 0)
					{
						pulseCount++;
						pulseLength ^= -1;
					}
				}
			}

			// get the pulses
			t.DataPeriods = new List<int>(pulseCount + 2);
			int tStateCount = 0;
			pulseLength = 0;
			for (int i = 1; i < samSize; i++)
			{
				for (int p = 0x80; p != 0; p >>= 1)
				{
					tStateCount += tStatesPerSample;
					if (((data[_position] ^ pulseLength) & p) != 0)
					{
						t.DataPeriods.Add(tStateCount);
						pulseLength ^= -1;
						tStateCount = 0;
					}
				}

				// increment position
				_position++;
			}

			// get the pulses in the last byte of data
			for (int p = 0x80; p != (byte)(0x80 >> usedBitsInLastByte); p >>= 1)
			{
				tStateCount += tStatesPerSample;
				if (((data[_position] ^ pulseLength) & p) != 0)
				{
					t.DataPeriods.Add(tStateCount);
					pulseLength ^= -1;
					tStateCount = 0;
				}
			}

			// add final pulse
			t.DataPeriods.Add(tStateCount);

			// add end of block pause
			if (pauseAfterBlock > 0)
			{
				//t.DataPeriods.Add(3500 * pauseAfterBlock);
			}

			t.PauseInMS = pauseAfterBlock;

			// increment position
			_position++;

			// add the block
			_datacorder.DataBlocks.Add(t);

			// generate PAUSE block
			CreatePauseBlock(_datacorder.DataBlocks.Last());
		}

		/*              length: [00,01,02,03]+04
				Offset	Value	Type	Description
				0x00	10+N	DWORD	Block length (without these four bytes)
				0x04	-	    WORD	Pause after this block (in ms).
				0x06	-	    BYTE[3]	Sampling rate
				0x09	-	    BYTE	Compression type
										0x01: RLE
										0x02: Z-RLE
				0x0A	-	    DWORD	Number of stored pulses (after decompression, for validation purposes)
				0x0E	-	    BYTE[N]	CSW data, encoded according to the CSW file format specification.

				This block contains a sequence of raw pulses encoded in CSW format v2 (Compressed Square Wave). */
		private void ProcessBlockID18(byte[] data)
		{
			TapeDataBlock t = new TapeDataBlock
			{
				BlockID = 0x18,
				BlockDescription = BlockType.CSW_Recording,
				DataPeriods = new List<int>()
			};

			int blockLen = GetInt32(data, _position);
			_position += 4;

			t.PauseInMS = GetWordValue(data, _position);

			_position += 2;

			int sampleRate = data[_position++] << 16 | data[_position++] << 8 | data[_position++];
			byte compType = data[_position++];
			int pulses = GetInt32(data, _position);
			_position += 4;

			int dataLen = blockLen - 10;

			// build source array
			byte[] src = new byte[dataLen];
			// build destination array
			byte[] dest = new byte[pulses + 1];

			// process the CSW data
			SinclairSpectrum.CswConverter.ProcessCSWV2(src, ref dest, compType, pulses);

			// create the periods
			var rate = (69888 * 50) / sampleRate;

			for (int i = 0; i < dest.Length;)
			{
				int length = dest[i++] * rate;
				if (length == 0)
				{
					length = GetInt32(dest, i) / rate;
					i += 4;
				}

				t.DataPeriods.Add(length);
			}

			// add closing period
			t.DataPeriods.Add((69888 * 50) / 10);

			_position += dataLen;
			//_position += blockLen;

			// add the block
			_datacorder.DataBlocks.Add(t);

			// generate PAUSE block
			CreatePauseBlock(_datacorder.DataBlocks.Last());
		}

		/*              length: [00,01,02,03]+04
				Offset	                    Value	Type	    Description
				0x00	                    -	    DWORD	    Block length (without these four bytes)
				0x04	                    -	    WORD	    Pause after this block (ms)
				0x06	                    TOTP	DWORD	    Total number of symbols in pilot/sync block (can be 0)
				0x0A	                    NPP	    BYTE	    Maximum number of pulses per pilot/sync symbol
				0x0B	                    ASP	    BYTE	    Number of pilot/sync symbols in the alphabet table (0=256)
				0x0C	                    TOTD	DWORD	    Total number of symbols in data stream (can be 0)
				0x10	                    NPD	    BYTE	    Maximum number of pulses per data symbol
				0x11	                    ASD	    BYTE	    Number of data symbols in the alphabet table (0=256)
				0x12	                    -	    SYMDEF[ASP]	Pilot and sync symbols definition table
																This field is present only if TOTP>0
				0x12+
				(2*NPP+1)*ASP	            -	    PRLE[TOTP]	Pilot and sync data stream
																This field is present only if TOTP>0
				0x12+
				(TOTP>0)*((2*NPP+1)*ASP)+
				TOTP*3	                    -	    SYMDEF[ASD]	Data symbols definition table
																This field is present only if TOTD>0
				0x12+
				(TOTP>0)*((2*NPP+1)*ASP)+
				TOTP*3+
				(2*NPD+1)*ASD	            -	    BYTE[DS]	Data stream
																This field is present only if TOTD>0                 

				This block has been specifically developed to represent an extremely wide range of data encoding techniques.
				The basic idea is that each loading component (pilot tone, sync pulses, data) is associated to a specific sequence 
				of pulses, where each sequence (wave) can contain a different number of pulses from the others. 
				In this way we can have a situation where bit 0 is represented with 4 pulses and bit 1 with 8 pulses.    

				----
				SYMDEF structure format
				Offset	Value	Type	Description
				0x00	-	    BYTE	Symbol flags
									b0-b1: starting symbol polarity
											00: opposite to the current level (make an edge, as usual) - default
											01: same as the current level (no edge - prolongs the previous pulse)
											10: force low level
											11: force high level
				0x01	-	    WORD[MAXP]	Array of pulse lengths.

				The alphabet is stored using a table where each symbol is a row of pulses. The number of columns (i.e. pulses) of the table is the 
				length of the longest sequence amongst all (MAXP=NPP or NPD, for pilot/sync or data blocks respectively); shorter waves are terminated by a 
				zero-length pulse in the sequence.
				Any number of data symbols is allowed, so we can have more than two distinct waves; for example, imagine a loader which writes two bits at a 
				time by encoding them with four distinct pulse lengths: this loader would have an alphabet of four symbols, each associated to a specific 
				sequence of pulses (wave).
				----
				----
				PRLE structure format
				Offset	Value	Type	Description
				0x00	-	    BYTE	Symbol to be represented
				0x01	-	    WORD	Number of repetitions

				Most commonly, pilot and sync are repetitions of the same pulse, thus they are represented using a very simple RLE encoding structure which stores 
				the symbol and the number of times it must be repeated.
				Each symbol in the data stream is represented by a string of NB bits of the block data, where NB = ceiling(Log2(ASD)). 
				Thus the length of the whole data stream in bits is NB*TOTD, or in bytes DS=ceil(NB*TOTD/8). 
				----                                                    */
		private void ProcessBlockID19(byte[] data)
		{
			// not currently implemented properly
			var t = new TapeDataBlock
			{
				BlockID = 0x19,
				BlockDescription = BlockType.Generalized_Data_Block,
				DataPeriods = new List<int>()
			};

			int blockLen = GetInt32(data, _position);
			_position += 4;

			int pause = GetWordValue(data, _position);
			_position += 2;

			int totp = GetInt32(data, _position);
			_position += 4;

			int npp = data[_position++];

			int asp = data[_position++];

			int totd = GetInt32(data, _position);
			_position += 4;

			int npd = data[_position++];

			int asd = data[_position++];

			// add the block
			_datacorder.DataBlocks.Add(t);

			// advance the position to the next block
			_position += blockLen;
		}

		/*              length: 02
				Offset	Value	Type	Description
				0x00	-	    WORD	Pause duration (ms.)

				This will make a silence (low amplitude level (0)) for a given time in milliseconds. If the value is 0 then the 
				emulator or utility should (in effect) STOP THE TAPE, i.e. should not continue loading until the user or emulator requests it.     */
		private void ProcessBlockID20(byte[] data)
		{
			TapeDataBlock t = new TapeDataBlock
			{
				BlockID = 0x20,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Pause_or_Stop_the_Tape
			};

			int pauseDuration = GetWordValue(data, _position);
			if (pauseDuration != 0)
			{
				//t.BlockDescription = "Pause: " + pauseDuration + " ms";
			}
			else
			{
				//t.BlockDescription = "[STOP THE TAPE]";
			}

			t.PauseInMS = pauseDuration;

			if (pauseDuration == 0)
			{
				// issue stop the tape command
				t.Command = TapeCommand.STOP_THE_TAPE;
				// add 1ms period
				//t.DataPeriods.Add(3500);
				//pauseDuration = -1;

			}
			else
			{
				// this is actually just a pause
				//pauseDuration = 3500 * pauseDuration;
				//t.DataPeriods.Add(pauseDuration);
			}

			// add end of block pause
			//t.DataPeriods.Add(pauseDuration);

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advanced position to next block
			_position += 2;

			// generate PAUSE block
			CreatePauseBlock(_datacorder.DataBlocks.Last());

		}

		/*              length: [00]+01
				Offset	Value	Type	Description
				0x00	L	BYTE	Length of the group name string
				0x01	-	CHAR[L]	Group name in ASCII format (please keep it under 30 characters long)                

				This block marks the start of a group of blocks which are to be treated as one single (composite) block. 
				This is very handy for tapes that use lots of subblocks like Bleepload (which may well have over 160 custom loading blocks). 
				You can also give the group a name (example 'Bleepload Block 1').
				For each group start block, there must be a group end block. Nesting of groups is not allowed.           */
		private void ProcessBlockID21(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x21,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Group_Start
			};

			int nameLength = data[_position];
			_position++;

			string name = Encoding.ASCII.GetString(data, _position, nameLength);
			//t.BlockDescription = "[GROUP: " + name + "]";
			t.Command = TapeCommand.BEGIN_GROUP;

			t.PauseInMS = 0;

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += nameLength;
		}

		/*              length: 00              

				This indicates the end of a group. This block has no body.           */
		private void ProcessBlockID22(byte[] data)
		{
			TapeDataBlock t = new TapeDataBlock
			{
				BlockID = 0x22,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Group_End,
				Command = TapeCommand.END_GROUP,
				PauseInMS = 0
			};

			// add to tape
			_datacorder.DataBlocks.Add(t);
		}

		/*              length: 02
				Offset	Value	Type	Description
				0x00	-	    WORD	Relative jump value              

				This block will enable you to jump from one block to another within the file. The value is a signed short word 
				(usually 'signed short' in C); Some examples:
					Jump 0 = 'Loop Forever' - this should never happen
					Jump 1 = 'Go to the next block' - it is like NOP in assembler ;)
					Jump 2 = 'Skip one block'
					Jump -1 = 'Go to the previous block'
				All blocks are included in the block count!.           */
		private void ProcessBlockID23(byte[] data)
		{
			// not implemented properly
			var t = new TapeDataBlock
			{
				BlockID = 0x23,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Jump_to_Block
			};

			int relativeJumpValue = GetWordValue(data, _position);
			string result = string.Empty;

			switch (relativeJumpValue)
			{
				case 0:
					result = "Loop Forever";
					break;
				case 1:
					result = "To Next Block";
					break;
				case 2:
					result = "Skip One Block";
					break;
				case -1:
					result = "Go to Previous Block";
					break;
			}

			//t.BlockDescription = "[JUMP BLOCK - " + result +"]";

			t.PauseInMS = 0;

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += 2;
		}

		/*              length: 02
				Offset	Value	Type	Description
				0x00	-	    WORD	Number of repetitions (greater than 1)           

				If you have a sequence of identical blocks, or of identical groups of blocks, you can use this block to tell how many times they should 
				be repeated. This block is the same as the FOR statement in BASIC.
				For simplicity reasons don't nest loop blocks!           */
		private void ProcessBlockID24(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x24,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Loop_Start
			};

			// loop should start from the next block
			int loopStart = _datacorder.DataBlocks.Count + 1;

			int numberOfRepetitions = GetWordValue(data, _position);

			// update loop counter
			_loopCounter.Add(
				new KeyValuePair<int, int>(
					loopStart,
					numberOfRepetitions));

			// update description
			//t.BlockDescription = "[LOOP START - " + numberOfRepetitions + " times]";

			t.PauseInMS = 0;

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += 2;
		}

		/*              length: 00    

				This is the same as BASIC's NEXT statement. It means that the utility should jump back to the start of the loop if it hasn't 
				been run for the specified number of times.
				This block has no body.          */
		private void ProcessBlockID25(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x25,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Loop_End
			};

			// get the most recent loop info
			var loop = _loopCounter.LastOrDefault();

			int loopStart = loop.Key;
			int numberOfRepetitions = loop.Value;

			if (numberOfRepetitions == 0)
			{
				return;
			}

			// get the number of blocks to loop
			int blockCnt = _datacorder.DataBlocks.Count - loopStart;

			// loop through each group to repeat
			for (int b = 0; b < numberOfRepetitions; b++)
			{
				TapeDataBlock repeater = new TapeDataBlock();
				//repeater.BlockDescription = "[LOOP REPEAT - " + (b + 1) + "]";
				repeater.DataPeriods = new List<int>();

				// add the repeat block
				_datacorder.DataBlocks.Add(repeater);

				// now iterate through and add the blocks to be repeated
				for (int i = 0; i < blockCnt; i++)
				{
					var block = _datacorder.DataBlocks[loopStart + i];
					_datacorder.DataBlocks.Add(block);
				}
			}
		}

		/*              length: [00,01]*02+02
				Offset	Value	Type	Description
				0x00	N	    WORD	Number of calls to be made
				0x02	-	    WORD[N]	Array of call block numbers (relative-signed offsets)    

				This block is an analogue of the CALL Subroutine statement. It basically executes a sequence of blocks that are somewhere else and 
				then goes back to the next block. Because more than one call can be normally used you can include a list of sequences to be called. 
				The 'nesting' of call blocks is also not allowed for the simplicity reasons. You can, of course, use the CALL blocks in the LOOP 
				sequences and vice versa. The value is relative for the obvious reasons - so that you can add some blocks in the beginning of the 
				file without disturbing the call values. Please take a look at 'Jump To Block' for reference on the values.          */
		private void ProcessBlockID26(byte[] data)
		{
			// block processing not implemented for this - just gets added for informational purposes only
			var t = new TapeDataBlock
			{
				BlockID = 0x26,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Call_Sequence
			};

			int blockSize = 2 + 2 * GetWordValue(data, _position);
			t.PauseInMS = 0;


			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += blockSize;
		}

		/*              length: 00  

				This block indicates the end of the Called Sequence. The next block played will be the block after the last CALL block (or the next Call, 
				if the Call block had multiple calls).
				Again, this block has no body.          */
		private void ProcessBlockID27(byte[] data)
		{
			// block processing not implemented for this - just gets added for informational purposes only
			var t = new TapeDataBlock
			{
				BlockID = 0x27,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Return_From_Sequence,
				PauseInMS = 0
			};

			// add to tape
			_datacorder.DataBlocks.Add(t);
		}

		/*              length: [00,01]+02
				Offset	Value	Type	Description
				0x00	-	    WORD	Length of the whole block (without these two bytes)
				0x02	N	    BYTE	Number of selections
				0x03	-	    SELECT[N]	List of selections  

				----
				SELECT structure format
				Offset	Value	Type	Description
				0x00	-	    WORD	Relative Offset
				0x02	L	    BYTE	Length of description text
				0x03	-	    CHAR[L]	Description text (please use single line and max. 30 chars)
				----

				This block is useful when the tape consists of two or more separately-loadable parts. With this block, you are able to select 
				one of the parts and the utility/emulator will start loading from that block. For example you can use it when the game has a 
				separate Trainer or when it is a multiload. Of course, to make some use of it the emulator/utility has to show a menu with the 
				selections when it encounters such a block. All offsets are relative signed words.          */
		private void ProcessBlockID28(byte[] data)
		{
			// block processing not implemented for this - just gets added for informational purposes only
			var t = new TapeDataBlock
			{
				BlockID = 0x28,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Select_Block
			};

			int blockSize = 2 + GetWordValue(data, _position);

			t.PauseInMS = 0;

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += blockSize;
		}

		/*              length: 04
						Offset	Value	Type	Description
						0x00	0	    DWORD	Length of the block without these four bytes (0)

						When this block is encountered, the tape will stop ONLY if the machine is an 48K Spectrum. This block is to be used for 
						multiloading games that load one level at a time in 48K mode, but load the entire tape at once if in 128K mode.
						This block has no body of its own, but follows the extension rule.          */
		private void ProcessBlockID2A(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x2A,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Stop_the_Tape_48K,
				Command = TapeCommand.STOP_THE_TAPE_48K
			};

			int blockSize = 4 + GetWordValue(data, _position);

			t.PauseInMS = 0;

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += blockSize;
		}

		/*              length: 05
				Offset	Value	Type	Description
				0x00	1	    DWORD	Block length (without these four bytes)
				0x04	-	    BYTE	Signal level (0=low, 1=high)

				This block sets the current signal level to the specified value (high or low). It should be used whenever it is necessary to avoid any 
				ambiguities, e.g. with custom loaders which are level-sensitive.         */
		private void ProcessBlockID2B(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x2B,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Set_Signal_Level,
				PauseInMS = 0
			};

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += 5;
		}

		/*              length: [00]+01
				Offset	Value	Type	Description
				0x00	N	    BYTE	Length of the text description
				0x01	-	    CHAR[N]	Text description in ASCII format

				This is meant to identify parts of the tape, so you know where level 1 starts, where to rewind to when the game ends, etc. 
				This description is not guaranteed to be shown while the tape is playing, but can be read while browsing the tape or changing 
				the tape pointer.
				The description can be up to 255 characters long but please keep it down to about 30 so the programs can show it in one line 
				(where this is appropriate).
				Please use 'Archive Info' block for title, authors, publisher, etc.        */
		private void ProcessBlockID30(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x30,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Text_Description
			};

			int textLen = data[_position];
			_position++;

			string desc = Encoding.ASCII.GetString(data, _position, textLen);

			t.PauseInMS = 0;

			//t.BlockDescription = "[" + desc + "]";

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += textLen;
		}

		/*              length: [01]+02
				Offset	Value	Type	Description
				0x00	-	    BYTE	Time (in seconds) for which the message should be displayed
				0x01	N	    BYTE	Length of the text message
				0x02	-	    CHAR[N]	Message that should be displayed in ASCII format

				This will enable the emulators to display a message for a given time. This should not stop the tape and it should not make silence. 
				If the time is 0 then the emulator should wait for the user to press a key.
				The text message should:
					stick to a maximum of 30 chars per line;
					use single 0x0D (13 decimal) to separate lines;
					stick to a maximum of 8 lines.
				If you do not obey these rules, emulators may display your message in any way they like.        */
		private void ProcessBlockID31(byte[] data)
		{
			// currently not implemented properly in CPCHawk
			var t = new TapeDataBlock
			{
				BlockID = 0x31,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Message_Block
			};

			_position++;

			int msgLen = data[_position];
			_position++;

			string desc = Encoding.ASCII.GetString(data, _position, msgLen);

			t.Command = TapeCommand.SHOW_MESSAGE;

			//t.BlockDescription = "[MESSAGE: " + desc + "]";

			t.PauseInMS = 0;

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += msgLen;
		}

		/*              length: [00,01]+02
				Offset	Value	Type	Description
				0x00	-	    WORD	Length of the whole block (without these two bytes)
				0x02	N	    BYTE	Number of text strings
				0x03	-	    TEXT[N]	List of text strings

				----
				TEXT structure format
				Offset	Value	Type	Description
				0x00	-	    BYTE	Text identification byte:
											00 - Full title
											01 - Software house/publisher
											02 - Author(s)
											03 - Year of publication
											04 - Language
											05 - Game/utility type
											06 - Price
											07 - Protection scheme/loader
											08 - Origin
											FF - Comment(s)
				0x01	L	    BYTE	Length of text string
				0x02	-	    CHAR[L]	Text string in ASCII format
				----

				Use this block at the beginning of the tape to identify the title of the game, author, publisher, year of publication, price (including 
				the currency), type of software (arcade adventure, puzzle, word processor, ...), protection scheme it uses (Speedlock 1, Alkatraz, ...) 
				and its origin (Original, Budget re-release, ...), etc. This block is built in a way that allows easy future expansion. 
				The block consists of a series of text strings. Each text has its identification number (which tells us what the text means) and then 
				the ASCII text. To make it possible to skip this block, if needed, the length of the whole block is at the beginning of it.
				If all texts on the tape are in English language then you don't have to supply the 'Language' field
				The information about what hardware the tape uses is in the 'Hardware Type' block, so no need for it here.              */
		private void ProcessBlockID32(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x32,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Archive_Info
			};

			int blockLen = GetWordValue(data, _position);
			_position += 2;
			int stringCount = data[_position++];

			// iterate through each string
			for (int s = 0; s < stringCount; s++)
			{
				// identify the type of text
				int type = data[_position++];

				// get text length
				int strLen = data[_position++];

				string title = string.Empty;
				title = "Info: ";

				switch (type)
				{
					case 0x00:
						title = "Full Title: ";
						break;
					case 0x01:
						title = "Software House/Publisher: ";
						break;
					case 0x02:
						title = "Author(s): ";
						break;
					case 0x03:
						title = "Year of Publication: ";
						break;
					case 0x04:
						title = "Language: ";
						break;
					case 0x05:
						title = "Game/Utility Type: ";
						break;
					case 0x06:
						title = "Price: ";
						break;
					case 0x07:
						title = "Protection Scheme/Loader: ";
						break;
					case 0x08:
						title = "Origin: ";
						break;
					case 0xFF:
						title = "Comment(s): ";
						break;
				}

				// add title to description
				//t.BlockDescription += title;

				// get string data
				string val = Encoding.ASCII.GetString(data, _position, strLen);
				//t.BlockDescription += val + " \n";

				t.PauseInMS = 0;

				// advance to next string block
				_position += strLen;
			}

			// add to tape
			_datacorder.DataBlocks.Add(t);
		}

		/*              length: [00]*03+01
				Offset	Value	Type	Description
				0x00	N	    BYTE	Number of machines and hardware types for which info is supplied
				0x01	-	    HWINFO[N]	List of machines and hardware

				----
				HWINFO structure format
				Offset	Value	Type	Description
				0x00	-	    BYTE	Hardware type
				0x01	-	    BYTE	Hardware ID
				0x02	-	    BYTE	Hardware information:
											00 - The tape RUNS on this machine or with this hardware,
													but may or may not use the hardware or special features of the machine.
											01 - The tape USES the hardware or special features of the machine,
													such as extra memory or a sound chip.
											02 - The tape RUNS but it DOESN'T use the hardware
													or special features of the machine.
											03 - The tape DOESN'T RUN on this machine or with this hardware.
				----

				This blocks contains information about the hardware that the programs on this tape use. Please include only machines and hardware for 
				which you are 100% sure that it either runs (or doesn't run) on or with, or you know it uses (or doesn't use) the hardware or special 
				features of that machine.
				If the tape runs only on the ZX81 (and TS1000, etc.) then it clearly won't work on any Spectrum or Spectrum variant, so there's no 
				need to list this information.
				If you are not sure or you haven't tested a tape on some particular machine/hardware combination then do not include it in the list.
				The list of hardware types and IDs is somewhat large, and may be found at the end of the format description.              */
		private void ProcessBlockID33(byte[] data)
		{
			// currently not implemented properly in CPCHawk

			TapeDataBlock t = new TapeDataBlock
			{
				BlockID = 0x33,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Hardware_Type,
				PauseInMS = 0
			};

			// first byte contains number of HWINFOs
			int infos = data[_position];

			_position += 1;

			// now starts the HW infos (each block 3 bytes)
			for (int i = 0; i < infos; i++)
			{
				_position += 3;
			}

			// add to tape
			_datacorder.DataBlocks.Add(t);
		}

		/*              length: [10,11,12,13]+14
				Offset	Value	Type	Description
				0x00	-	    CHAR[10]	Identification string (in ASCII)
				0x10	L	    DWORD	Length of the custom info
				0x14	-	    BYTE[L]	Custom info                                

				This block can be used to save any information you want. For example, it might contain some information written by a utility, 
				extra settings required by a particular emulator, or even poke data.               */
		private void ProcessBlockID35(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x35,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Custom_Info_Block,
				PauseInMS = 0
			};

			string info = Encoding.ASCII.GetString(data, _position, 0x10);
			//t.BlockDescription = "[CUSTOM INFO: " + info + "]";
			_position += 0x10;

			int blockLen = BitConverter.ToInt32(data, _position);
			_position += 4;

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += blockLen;
		}

		/*              length: 09
				Offset	Value	Type	Description
				0x00	-	    BYTE[9]	Value: { "XTape!",0x1A,MajR,MinR } 
										Just skip these 9 bytes and you will end up on the next ID.                                

				This block is generated when you merge two ZX Tape files together. It is here so that you can easily copy the files together and use 
				them. Of course, this means that resulting file would be 10 bytes longer than if this block was not used. All you have to do 
				if you encounter this block ID is to skip next 9 bytes.
				If you can avoid using this block for this purpose, then do so; it is preferable to use a utility to join the two files and 
				ensure that they are both of the higher version number.               */
		private void ProcessBlockID5A(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = 0x5A,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Glue_Block,
				PauseInMS = 0
			};

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += 9;
		}

		private void ProcessUnidentifiedBlock(byte[] data)
		{
			var t = new TapeDataBlock
			{
				BlockID = -2,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Unsupported
			};
			//t.BlockDescription = "[UNSUPPORTED - 0x" + data[_position - 1]  + "]";

			_position += GetInt32(data, _position) & 0xFFFFFF;

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += 4;
		}

		// These mostly should be ignored by CPCHawk - here for completeness

		private void ProcessBlockID16(byte[] data)
		{
			// CPCHawk will not implement this block. it will however handle it so subsequent blocks can be parsed
			TapeDataBlock t = new TapeDataBlock
			{
				BlockID = 0x16,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.C64_ROM_Type_Data_Block,
				PauseInMS = 0
			};

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			int blockLen = GetInt32(data, _position);
			_position += blockLen;
		}

		private void ProcessBlockID17(byte[] data)
		{
			// CPCHawk will not implement this block. it will however handle it so subsequent blocks can be parsed
			var t = new TapeDataBlock
			{
				BlockID = 0x17,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.C64_Turbo_Tape_Data_Block,
				PauseInMS = 0
			};

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			int blockLen = GetInt32(data, _position);
			_position += blockLen;
		}

		private void ProcessBlockID34(byte[] data)
		{
			// currently not implemented properly in CPCHawk
			var t = new TapeDataBlock
			{
				BlockID = 0x34,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Emulation_Info
			};

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += 8;
		}

		/*              length: [01,02,03]+04
                Offset	Value	Type	Description
                0x00	-	    BYTE	Snapshot type:
                                            00: .Z80 format
                                            01: .SNA format
                0x01	L	    BYTE[3]	Snapshot length
                0x04	-	    BYTE[L]	Snapshot itself                               

                This would enable one to snapshot the game at the start and still have all the tape blocks (level data, etc.) in the same file. 
                Only .Z80 and .SNA snapshots are supported for compatibility reasons!
                The emulator should take care of that the snapshot is not taken while the actual Tape loading is taking place (which doesn't do much sense). 
                And when an emulator encounters the snapshot block it should load it and then continue with the next block.               */
		private void ProcessBlockID40(byte[] data)
		{
			// currently not implemented properly in CPCHawk

			TapeDataBlock t = new TapeDataBlock
			{
				BlockID = 0x40,
				DataPeriods = new List<int>(),
				BlockDescription = BlockType.Snapshot_Block
			};

			_position++;

			int blockLen = data[_position] |
				data[_position + 1] << 8 |
				data[_position + 2] << 16;
			_position += 3;

			// add to tape
			_datacorder.DataBlocks.Add(t);

			// advance to next block 
			_position += blockLen;
		}

		/// <summary>
		/// Used to process either a standard or turbo data block
		/// </summary>
		private TapeDataBlock DecodeDataBlock
			(
				TapeDataBlock block,
				byte[] blockdata,
				DataBlockType dataBlockType,
				int pauseAfterBlock,
				int pilotCount,
				int pilotToneLength = 2168,
				int sync1PulseLength = 667,
				int sync2PulseLength = 735,
				int bit0PulseLength = 855,
				int bit1PulseLength = 1710,
				int bitsInLastByte = 8
			)
		{

			// first get the block description
			string description = string.Empty;

			// process the type byte
			/*  (The type is 0,1,2 or 3 for a Program, Number array, Character array or Code file. 
                A SCREEN$ file is regarded as a Code file with start address 16384 and length 6912 decimal. 
                If the file is a Program file, parameter 1 holds the autostart line number (or a number >=32768 if no LINE parameter was given) 
                and parameter 2 holds the start of the variable area relative to the start of the program. If it's a Code file, parameter 1 holds 
                the start of the code block when saved, and parameter 2 holds 32768. For data files finally, the byte at position 14 decimal holds the variable name.)
            */

			int blockSize = blockdata.Length;

			// don't get description info for Pure Data Blocks
			if (dataBlockType != DataBlockType.Pure)
			{
				if (blockdata[0] == 0x00 && blockSize == 19)
				{
					string fileName = Encoding.ASCII.GetString(blockdata.Skip(2).Take(10).ToArray()).Trim();
					string type = "Unknown Type";
					StringBuilder sb = new StringBuilder();

					var param1 = GetWordValue(blockdata, 12);
					var param2 = GetWordValue(blockdata, 14);

					// header block - examine first byte of header
					if (blockdata[1] == 0)
					{
						type = "Program";
						sb.Append(type + ": ");
						sb.Append(fileName + " ");
					}
					else if (blockdata[1] == 1)
					{
						type = "NumArray";
						sb.Append(type + ": ");
						sb.Append(fileName + " ");
					}
					else if (blockdata[1] == 2)
					{
						type = "CharArray";
						sb.Append(type + ": ");
						sb.Append(fileName + " ");
					}
					else if (blockdata[1] == 3)
					{
						type = "Code";
						sb.Append(type + ": ");
						sb.Append(fileName + " ");
					}
				}
				else if (blockdata[0] == 0xff)
				{
					// data block
					description = "Data Block " + (blockSize - 2) + "bytes";
					block.AddMetaData(BlockDescriptorTitle.Data_Bytes, (blockSize - 2) + " Bytes");
				}
				else
				{
					// some other type (turbo data etc..)
					description = $"#{blockdata[0].ToString("X2")} block, {blockSize} bytes";
					//description += (crc != 0) ? $", crc bad (#{crcFile:X2}!=#{crcValue:X2})" : ", crc ok";
					block.AddMetaData(BlockDescriptorTitle.Undefined, description);
				}
				/*
                if (blockdata[0] == 0x00 && blockSize == 19 && (blockdata[1] == 0x00) || (blockdata[1] == 3 && blockdata.Length > 3))
                {
                    if (dataBlockType != DataBlockType.Turbo)
                    {
                        // This is the program header
                        string fileName = Encoding.ASCII.GetString(blockdata.Skip(2).Take(10).ToArray()).Trim();

                        string type = "";
                        if (blockdata[0] == 0x00)
                        {
                            type = "Program";
                            block.AddMetaData(BlockDescriptorTitle.Program, fileName);
                        }
                        else
                        {
                            type = "Bytes";
                            block.AddMetaData(BlockDescriptorTitle.Bytes, fileName);
                        }

                        // now build the description string
                        StringBuilder sb = new StringBuilder();
                        sb.Append(type + ": ");
                        sb.Append(fileName + " ");
                        sb.Append(GetWordValue(blockdata, 14));
                        sb.Append(':');
                        sb.Append(GetWordValue(blockdata, 12));
                        description = sb.ToString();
                    }
                }
                else if (blockdata[0] == 0xFF)
                {
                    // this is a data block
                    description = "Data Block " + (blockSize - 2) + "bytes";
                    block.AddMetaData(BlockDescriptorTitle.Data_Bytes, (blockSize - 2).ToString() + " Bytes");
                }
                else
                {
                    // other type
                    description = $"#{blockdata[0]:X2} block, {blockSize} bytes";
                    //description += (crc != 0) ? $", crc bad (#{crcFile:X2}!=#{crcValue:X2})" : ", crc ok";
                    block.AddMetaData(BlockDescriptorTitle.Undefined, description);
                }  
                */
			}

			// update metadata
			switch (dataBlockType)
			{
				case DataBlockType.Standard:
				case DataBlockType.Turbo:

					if (dataBlockType == DataBlockType.Standard)
						block.BlockDescription = BlockType.Standard_Speed_Data_Block;
					if (dataBlockType == DataBlockType.Turbo)
						block.BlockDescription = BlockType.Turbo_Speed_Data_Block;

					block.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Length, pilotToneLength + " T-States");
					block.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Count, pilotCount + " Pulses");
					block.AddMetaData(BlockDescriptorTitle.First_Sync_Length, sync1PulseLength + " T-States");
					block.AddMetaData(BlockDescriptorTitle.Second_Sync_Length, sync2PulseLength + " T-States");
					break;

				case DataBlockType.Pure:
					block.BlockDescription = BlockType.Pure_Data_Block;
					break;
			}

			block.AddMetaData(BlockDescriptorTitle.Zero_Bit_Length, bit0PulseLength + " T-States");
			block.AddMetaData(BlockDescriptorTitle.One_Bit_Length, bit1PulseLength + " T-States");
			block.AddMetaData(BlockDescriptorTitle.Data_Length, blockSize + " Bytes");
			block.AddMetaData(BlockDescriptorTitle.Bits_In_Last_Byte, bitsInLastByte + " Bits");
			block.AddMetaData(BlockDescriptorTitle.Pause_After_Data, pauseAfterBlock + " ms");

			// calculate period information
			List<int> dataPeriods = new List<int>();

			// generate pilot pulses

			if (pilotCount > 0)
			{
				for (int i = 0; i < pilotCount; i++)
				{
					dataPeriods.Add(pilotToneLength);
				}

				// add syncro pulses
				dataPeriods.Add(sync1PulseLength);
				dataPeriods.Add(sync2PulseLength);
			}

			int pos = 0;

			// add bit0 and bit1 periods
			for (int i = 0; i < blockSize - 1; i++, pos++)
			{
				for (byte b = 0x80; b != 0; b >>= 1)
				{
					if ((blockdata[i] & b) != 0)
						dataPeriods.Add(bit1PulseLength);
					else
						dataPeriods.Add(bit0PulseLength);
					if ((blockdata[i] & b) != 0)
						dataPeriods.Add(bit1PulseLength);
					else
						dataPeriods.Add(bit0PulseLength);
				}
			}

			// add the last byte
			for (byte c = 0x80; c != (byte)(0x80 >> bitsInLastByte); c >>= 1)
			{
				if ((blockdata[pos] & c) != 0)
					dataPeriods.Add(bit1PulseLength);
				else
					dataPeriods.Add(bit0PulseLength);
				if ((blockdata[pos] & c) != 0)
					dataPeriods.Add(bit1PulseLength);
				else
					dataPeriods.Add(bit0PulseLength);
			}

			// add block pause if pause is not 0
			if (pauseAfterBlock != 0)
			{
				block.PauseInMS = pauseAfterBlock;
				//int actualPause = pauseAfterBlock * 3500;
				//dataPeriods.Add(actualPause);
			}

			// add to the tapedatablock object
			block.DataPeriods = dataPeriods;

			// add the raw data
			block.BlockData = blockdata;

			return block;
		}

		/// <summary>
		/// Used to process either a standard or turbo data block
		/// </summary>
		private TapeDataBlock DecodeDataBlock
			(
				TapeDataBlock block,
				byte[] blockData,
				DataBlockType dataBlockType,
				int pauseAfterBlock,
				int pilotToneLength = 2168,
				int sync1PulseLength = 667,
				int sync2PulseLength = 735,
				int bit0PulseLength = 855,
				int bit1PulseLength = 1710,
				int bitsInLastByte = 8
			)
		{


			int pilotCount = 3220;
			/*
            // pilot count needs to be ascertained from flag byte
            int pilotCount;
            if (blockData[0] < 128)
                pilotCount = 8063;
            else
                pilotCount = 3223;
                */

			// now we can decode
			var nBlock = DecodeDataBlock
				(
					block,
					blockData,
					dataBlockType,
					pauseAfterBlock,
					pilotCount,
					pilotToneLength,
					sync1PulseLength,
					sync2PulseLength,
					bit0PulseLength,
					bit1PulseLength,
					bitsInLastByte
				);


			return nBlock;
		}

		/// <summary>
		/// If necessary a separate PAUSE block will be created
		/// </summary>
		private void CreatePauseBlock(TapeDataBlock original)
		{
			if (original.PauseInMS > 0)
			{
				var pBlock = new TapeDataBlock
				{
					DataPeriods = new List<int>(),
					BlockDescription = BlockType.PAUSE_BLOCK,
					PauseInMS = 0
				};
				var pauseInTStates = TranslatePause(original.PauseInMS);

				pBlock.AddMetaData(BlockDescriptorTitle.Block_ID, pauseInTStates + " cycles");

				int by1000 = pauseInTStates / 70000;
				int rem1000 = pauseInTStates % 70000;

				if (by1000 > 1)
				{
					pBlock.DataPeriods.Add(35000);
					pBlock.DataPeriods.Add(pauseInTStates - 35000);
				}
				else
				{
					pBlock.DataPeriods.Add(pauseInTStates);
					pBlock.DataPeriods.Add(0);
				}

				_datacorder.DataBlocks.Add(pBlock);
			}
		}
	}

	public enum DataBlockType
	{
		Standard,
		Turbo,
		Pure
	}
}
