using System;
using System.IO;
using System.Text;

namespace m3uTool
{
	/// <summary>
	/// Indicates that a problem existed when parsing an mp3 file
	/// </summary>
	public class Mp3FormatException : Exception
	{
		string mFilename;
		
		string mFormatExceptionString;

		public string Filename
		{
			get { return mFilename; }
		}

		public string FormatExceptionString
		{
			get { return mFormatExceptionString; }
		}

		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="filename"></param>
		/// <param name="formatExceptionString"></param>
		public Mp3FormatException(string filename, string formatExceptionString)
		{
			mFilename = filename;
			mFormatExceptionString = formatExceptionString;
		}
	}
	
	/* ----------------------------------------------------------

	   original C++ code by:
							Gustav "Grim Reaper" Munkby
							http://floach.pimpin.net/grd/
							grimreaperdesigns@gmx.net
   
	   modified and converted to C# by:
							Robert A. Wlodarczyk
							http://rob.wincereview.com:8080
							rwlodarc@hotmail.com
	   ---------------------------------------------------------- */
	public class Mp3Header
	{
		//// Public variables for storing the information about the MP3
		//public int intBitRate;
		//public string strFileName;
		//public long lngFileSize;
		//public int intFrequency;
		//public string strMode;
		//public int intLength;
		//public string strLengthFormatted;


		// Private variables used in the process of reading in the MP3 files
		private ulong bithdr;
		private bool boolVBitRate;
		private int intVFrames;

		public Mp3FileDetails ReadMP3Information(string fileName)
		{
			mFileDetails = new Mp3FileDetails();
			FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
			// Set the filename not including the path information
			mFileDetails.strFileName = @fs.Name;
			char[] chrSeparators = new char[]{'\\','/'};
			string[] strSeparator = mFileDetails.strFileName.Split(chrSeparators);
			int intUpper = strSeparator.GetUpperBound(0);
			mFileDetails.strFileName = strSeparator[intUpper];
        
			// Replace ' with '' for the SQL INSERT statement
			mFileDetails.strFileName = mFileDetails.strFileName.Replace("'", "''");
        
			// Set the file size
			mFileDetails.lngFileSize = fs.Length;

			byte[] bytHeader = new byte[4];
			byte[] bytVBitRate = new byte[12];
			int intPos = 0;
        
			// Keep reading 4 bytes from the header until we know for sure that in 
			// fact it's an MP3
			do
			{
				fs.Position = intPos;
				fs.Read(bytHeader,0,4);
				intPos++;
				LoadMP3Header(bytHeader);
			}
			while(!IsValidHeader() && (fs.Position!=fs.Length));
        
			// If the current file stream position is equal to the length, 
			// that means that we've read the entire file and it's not a valid MP3 file
			if(fs.Position != fs.Length)
			{
				intPos += 3;

				if(getVersionIndex() == 3)    // MPEG Version 1
				{
					if(getModeIndex() == 3)    // Single Channel
					{
						intPos += 17;
					}
					else
					{
						intPos += 32;
					}
				}
				else                        // MPEG Version 2.0 or 2.5
				{
					if(getModeIndex() == 3)    // Single Channel
					{
						intPos += 9;
					}
					else
					{
						intPos += 17;
					}
				}
            
				// Check to see if the MP3 has a variable bitrate
				fs.Position = intPos;
				fs.Read(bytVBitRate,0,12);
				boolVBitRate = LoadVBRHeader(bytVBitRate);

				// Once the file's read in, then assign the properties of the file to the public variables
				mFileDetails.intBitRate = getBitrate();
				mFileDetails.intFrequency = getFrequency();
				mFileDetails.strMode = getMode();
				mFileDetails.intLength = getLengthInSeconds();
				mFileDetails.strLengthFormatted = getFormattedLength();
				fs.Close();
				return mFileDetails;
			}
			throw new Mp3FormatException(fileName, "Invalid mp3 file");
		}

		private void LoadMP3Header(byte[] c)
		{
			// this thing is quite interesting, it works like the following
			// c[0] = 00000011
			// c[1] = 00001100
			// c[2] = 00110000
			// c[3] = 11000000
			// the operator << means that we'll move the bits in that direction
			// 00000011 << 24 = 00000011000000000000000000000000
			// 00001100 << 16 =         000011000000000000000000
			// 00110000 << 24 =                 0011000000000000
			// 11000000       =                         11000000
			//                +_________________________________
			//                  00000011000011000011000011000000
			bithdr = (ulong)(((c[0] & 255) << 24) | ((c[1] & 255) << 16) | ((c[2] & 255) <<  8) | ((c[3] & 255))); 
		}

		private bool LoadVBRHeader(byte[] inputheader)
		{
			// If it's a variable bitrate MP3, the first 4 bytes will read 'Xing'
			// since they're the ones who added variable bitrate-edness to MP3s
			if(inputheader[0] == 88 && inputheader[1] == 105 && 
				inputheader[2] == 110 && inputheader[3] == 103)
			{
				int flags = (((inputheader[4] & 255) << 24) | ((inputheader[5] & 255) << 16) | ((inputheader[6] & 255) <<  8) | ((inputheader[7] & 255)));
				if((flags & 0x0001) == 1)
				{
					intVFrames = (((inputheader[8] & 255) << 24) | ((inputheader[9] & 255) << 16) | ((inputheader[10] & 255) <<  8) | ((inputheader[11] & 255)));
					return true;
				}
				else
				{
					intVFrames = -1;
					return true;
				}
			}
			return false;
		}

		private bool IsValidHeader() 
		{
			return (((getFrameSync()      & 2047)==2047) &&
				((getVersionIndex()   &    3)!=   1) &&
				((getLayerIndex()     &    3)!=   0) && 
				((getBitrateIndex()   &   15)!=   0) &&
				((getBitrateIndex()   &   15)!=  15) &&
				((getFrequencyIndex() &    3)!=   3) &&
				((getEmphasisIndex()  &    3)!=   2)    );
		}

		private int getFrameSync()     
		{
			return (int)((bithdr>>21) & 2047); 
		}

		private int getVersionIndex()  
		{ 
			return (int)((bithdr>>19) & 3);  
		}

		private int getLayerIndex()    
		{ 
			return (int)((bithdr>>17) & 3);  
		}

		//private int getProtectionBit() 
		//{ 
		//    return (int)((bithdr>>16) & 1);  
		//}

		private int getBitrateIndex()  
		{ 
			return (int)((bithdr>>12) & 15); 
		}

		private int getFrequencyIndex()
		{ 
			return (int)((bithdr>>10) & 3);  
		}

		//private int getPaddingBit()    
		//{ 
		//    return (int)((bithdr>>9) & 1);  
		//}

		//private int getPrivateBit()    
		//{ 
		//    return (int)((bithdr>>8) & 1);  
		//}

		private int getModeIndex()     
		{ 
			return (int)((bithdr>>6) & 3);  
		}

		//private int getModeExtIndex()  
		//{ 
		//    return (int)((bithdr>>4) & 3);  
		//}

		//private int getCoprightBit()   
		//{ 
		//    return (int)((bithdr>>3) & 1);  
		//}

		//private int getOrginalBit()    
		//{ 
		//    return (int)((bithdr>>2) & 1);  
		//}
    
		private int getEmphasisIndex() 
		{ 
			return (int)(bithdr & 3);  
		}

		//private double getVersion() 
		//{
		//    double[] table = {2.5, 0.0, 2.0, 1.0};
		//    return table[getVersionIndex()];
		//}

		//private int getLayer() 
		//{
		//    return (int)(4 - getLayerIndex());
		//}

		private int getBitrate() 
		{
			// If the file has a variable bitrate, then we return an integer average bitrate,
			// otherwise, we use a lookup table to return the bitrate
			if(boolVBitRate)
			{
				double medFrameSize = (double)mFileDetails.lngFileSize/ (double)getNumberOfFrames();
				return (int)((medFrameSize * getFrequency()) / (1000.0 * ((getLayerIndex()==3) ? 12.0 : 144.0)));
			}
			else
			{
				int[,,] table = {
					{ // MPEG 2 & 2.5
						{0,  8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0}, // Layer III
						{0,  8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0}, // Layer II
						{0, 32, 48, 56, 64, 80, 96,112,128,144,160,176,192,224,256,0}  // Layer I
					},
					{ // MPEG 1
						{0, 32, 40, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,0}, // Layer III
						{0, 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384,0}, // Layer II
						{0, 32, 64, 96,128,160,192,224,256,288,320,352,384,416,448,0}  // Layer I
					}
				};

				return table[getVersionIndex() & 1, getLayerIndex()-1, getBitrateIndex()];
			}
		}

		private int getFrequency() 
		{
			int[,] table =    {    
							{32000, 16000,  8000}, // MPEG 2.5
							{    0,     0,     0}, // reserved
							{22050, 24000, 16000}, // MPEG 2
							{44100, 48000, 32000}  // MPEG 1
							  };

			return table[getVersionIndex(), getFrequencyIndex()];
		}

		private string getMode() 
		{
			switch(getModeIndex()) 
			{
				default:
					return "Stereo";
				case 1:
					return "Joint Stereo";
				case 2:
					return "Dual Channel";
				case 3:
					return "Single Channel";
			}
		}

		private int getLengthInSeconds() 
		{
			// "intKilBitFileSize" made by dividing by 1000 in order to match the "Kilobits/second"
			int intKiloBitFileSize = ((8 * (int)mFileDetails.lngFileSize) / 1000);
			return (intKiloBitFileSize/getBitrate());
		}

		private string getFormattedLength() 
		{
			// Complete number of seconds
			int s  = getLengthInSeconds();

			// Seconds to display
			int ss = s%60;

			// Complete number of minutes
			int m  = (s-ss)/60;

			// Minutes to display
			int mm = m%60;

			// Complete number of hours
			int h = (m-mm)/60;

			// Make "hh:mm:ss"
			return h.ToString("D2") + ":" + mm.ToString("D2") + ":" + ss.ToString("D2");
		}

		private int getNumberOfFrames() 
		{
			// Again, the number of MPEG frames is dependant on whether it's a variable bitrate MP3 or not
			if (!boolVBitRate) 
			{
				double medFrameSize = (((getLayerIndex()==3) ? 12 : 144) *((1000.0 * (float)getBitrate())/(float)getFrequency()));
				return (int)(mFileDetails.lngFileSize/medFrameSize);
			}
			else 
				return intVFrames;
		}

		public static void ReadMP3Tag(ref Mp3FileDetails paramMP3)
		{
			//	Read the	128 byte	ID3 tag into a	byte array
			FileStream oFileStream;
			oFileStream = new FileStream(paramMP3.fileComplete, FileMode.Open);
			byte[] bBuffer = new byte[128];
			oFileStream.Seek(-128, SeekOrigin.End);
			oFileStream.Read(bBuffer, 0, 128);
			oFileStream.Close();

			//	Convert the	Byte Array to a String
			Encoding instEncoding = new ASCIIEncoding();	//	NB: Encoding is an Abstract class
			string id3Tag = instEncoding.GetString(bBuffer);

			//	If	there	is	an	attched ID3	v1.x TAG	then read it 
			if (id3Tag.Substring(0, 3) == "TAG")
			{
				paramMP3.id3Title = id3Tag.Substring(3, 30).Trim();
				paramMP3.id3Artist = id3Tag.Substring(33, 30).Trim();
				paramMP3.id3Album = id3Tag.Substring(63, 30).Trim();
				paramMP3.id3Year = id3Tag.Substring(93, 4).Trim();
				paramMP3.id3Comment = id3Tag.Substring(97, 28).Trim();

				// Get the	track	number if TAG conforms to ID3	v1.1
				if (id3Tag[125] == 0)
					paramMP3.id3TrackNumber = bBuffer[126];
				else
					paramMP3.id3TrackNumber = 0;
				paramMP3.id3Genre = bBuffer[127];
				paramMP3.hasID3Tag = true;
				// ********* IF USED IN ANGER: ENSURE	to	test for	non-numeric	year
			}
			else
			{
				// ID3 Tag	not found so create an empty TAG	in	case the	user saces later
				paramMP3.id3Title = "";
				paramMP3.id3Artist = "";
				paramMP3.id3Album = "";
				paramMP3.id3Year = "";
				paramMP3.id3Comment = "";
				paramMP3.id3TrackNumber = 0;
				paramMP3.id3Genre = 0;
				paramMP3.hasID3Tag = false;
			}
		}

		public static void UpdateMP3ID3Tag(ref Mp3FileDetails paramMP3)
		{
			//	Trim any	whitespace
			paramMP3.id3Title = paramMP3.id3Title.Trim();
			paramMP3.id3Artist = paramMP3.id3Artist.Trim();
			paramMP3.id3Album = paramMP3.id3Album.Trim();
			paramMP3.id3Year = paramMP3.id3Year.Trim();
			paramMP3.id3Comment = paramMP3.id3Comment.Trim();

			//	Ensure all properties are correct size
			if (paramMP3.id3Title.Length > 30) paramMP3.id3Title = paramMP3.id3Title.Substring(0, 30);
			if (paramMP3.id3Artist.Length > 30) paramMP3.id3Artist = paramMP3.id3Artist.Substring(0, 30);
			if (paramMP3.id3Album.Length > 30) paramMP3.id3Album = paramMP3.id3Album.Substring(0, 30);
			if (paramMP3.id3Year.Length > 4) paramMP3.id3Year = paramMP3.id3Year.Substring(0, 4);
			if (paramMP3.id3Comment.Length > 28) paramMP3.id3Comment = paramMP3.id3Comment.Substring(0, 28);

			//	Build	a new	ID3 Tag (128 Bytes)
			byte[] tagByteArray = new byte[128];
			for (int i = 0; i < tagByteArray.Length; i++)
				tagByteArray[i] = 0;	//	Initialise array to nulls

			//	Convert the	Byte Array to a String
			Encoding instEncoding = new ASCIIEncoding();	//	NB: Encoding is an Abstract class // ************ To DO:	Make a shared instance of ASCIIEncoding so we don't keep	creating/destroying it
			//	Copy "TAG" to Array
			byte[] workingByteArray = instEncoding.GetBytes("TAG");
			Array.Copy(workingByteArray, 0, tagByteArray, 0, workingByteArray.Length);
			//	Copy Title to Array
			workingByteArray = instEncoding.GetBytes(paramMP3.id3Title);
			Array.Copy(workingByteArray, 0, tagByteArray, 3, workingByteArray.Length);
			//	Copy Artist	to	Array
			workingByteArray = instEncoding.GetBytes(paramMP3.id3Artist);
			Array.Copy(workingByteArray, 0, tagByteArray, 33, workingByteArray.Length);
			//	Copy Album to Array
			workingByteArray = instEncoding.GetBytes(paramMP3.id3Album);
			Array.Copy(workingByteArray, 0, tagByteArray, 63, workingByteArray.Length);
			//	Copy Year to Array
			workingByteArray = instEncoding.GetBytes(paramMP3.id3Year);
			Array.Copy(workingByteArray, 0, tagByteArray, 93, workingByteArray.Length);
			//	Copy Comment to Array
			workingByteArray = instEncoding.GetBytes(paramMP3.id3Comment);
			Array.Copy(workingByteArray, 0, tagByteArray, 97, workingByteArray.Length);
			//	Copy Track and	Genre	to	Array
			tagByteArray[126] = paramMP3.id3TrackNumber;
			tagByteArray[127] = paramMP3.id3Genre;

			//	SAVE TO DISK: Replace the final 128	Bytes	with our	new ID3 tag
			FileStream oFileStream = new FileStream(paramMP3.fileComplete, FileMode.Open);
			if (paramMP3.hasID3Tag)
				oFileStream.Seek(-128, SeekOrigin.End);
			else
				oFileStream.Seek(0, SeekOrigin.End);
			oFileStream.Write(tagByteArray, 0, 128);
			oFileStream.Close();
			paramMP3.hasID3Tag = true;
		}
	}
}
