/*
 * MpegAudioFileReader.
 *
 * 12/31/04 : mp3spi.weak system property added to skip controls.
 * 
 * 11/29/04 : ID3v2.2, v2.3 & v2.4 support improved.
 *            "mp3.id3tag.composer" (TCOM/TCM) added
 *            "mp3.id3tag.grouping" (TIT1/TT1) added
 *            "mp3.id3tag.disc" (TPA/TPOS) added
 *            "mp3.id3tag.encoded" (TEN/TENC) added
 *            "mp3.id3tag.v2.version" added
 * 
 * 11/28/04 : String encoding bug fix in chopSubstring method.
 *  
 * JavaZOOM : mp3spi@javazoom.net
 * 			  http://www.javazoom.net
 *  
 *-----------------------------------------------------------------------
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------
 */

package	javazoom.spi.mpeg.sampled.file;


import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessControlException;
import java.util.HashMap;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;

import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.Header;
import javazoom.spi.mpeg.sampled.file.tag.IcyInputStream;
import javazoom.spi.mpeg.sampled.file.tag.MP3Tag;

import org.tritonus.share.TDebug;
import org.tritonus.share.sampled.file.TAudioFileReader;

/**
 * This class implements AudioFileReader for MP3 SPI.
 */
public class MpegAudioFileReader extends TAudioFileReader
{
	private final int		SYNC = 0xFFE00000;
	private String weak = null;
	private final AudioFormat.Encoding[][]	sm_aEncodings =
	{
		{MpegEncoding.MPEG2L1, MpegEncoding.MPEG2L2, MpegEncoding.MPEG2L3},
		{MpegEncoding.MPEG1L1, MpegEncoding.MPEG1L2, MpegEncoding.MPEG1L3},
		{MpegEncoding.MPEG2DOT5L1, MpegEncoding.MPEG2DOT5L2, MpegEncoding.MPEG2DOT5L3},

	};

	private static final int	INITAL_READ_LENGTH = 64000;
	private static final int	MARK_LIMIT = INITAL_READ_LENGTH + 1;

	private static final String[] id3v1genres = {
	  "Blues"
	  , "Classic Rock"
	  , "Country"
	  , "Dance"
	  , "Disco"
	  , "Funk"
	  , "Grunge"
	  , "Hip-Hop"
	  , "Jazz"
	  , "Metal"
	  , "New Age"
	  , "Oldies"
	  , "Other"
	  , "Pop"
	  , "R&B"
	  , "Rap"
	  , "Reggae"
	  , "Rock"
	  , "Techno"
	  , "Industrial"
	  , "Alternative"
	  , "Ska"
	  , "Death Metal"
	  , "Pranks"
	  , "Soundtrack"
	  , "Euro-Techno"
	  , "Ambient"
	  , "Trip-Hop"
	  , "Vocal"
	  , "Jazz+Funk"
	  , "Fusion"
	  , "Trance"
	  , "Classical"
	  , "Instrumental"
	  , "Acid"
	  , "House"
	  , "Game"
	  , "Sound Clip"
	  , "Gospel"
	  , "Noise"
	  , "AlternRock"
	  , "Bass"
	  , "Soul"
	  , "Punk"
	  , "Space"
	  , "Meditative"
	  , "Instrumental Pop"
	  , "Instrumental Rock"
	  , "Ethnic"
	  , "Gothic"
	  , "Darkwave"
	  , "Techno-Industrial"
	  , "Electronic"
	  , "Pop-Folk"
	  , "Eurodance"
	  , "Dream"
	  , "Southern Rock"
	  , "Comedy"
	  , "Cult"
	  , "Gangsta"
	  , "Top 40"
	  , "Christian Rap"
	  , "Pop/Funk"
	  , "Jungle"
	  , "Native American"
	  , "Cabaret"
	  , "New Wave"
	  , "Psychadelic"
	  , "Rave"
	  , "Showtunes"
	  , "Trailer"
	  , "Lo-Fi"
	  , "Tribal"
	  , "Acid Punk"
	  , "Acid Jazz"
	  , "Polka"
	  , "Retro"
	  , "Musical"
	  , "Rock & Roll"
	  , "Hard Rock"
	  , "Folk"
	  , "Folk-Rock"
	  , "National Folk"
	  , "Swing"
	  , "Fast Fusion"
	  , "Bebob"
	  , "Latin"
	  , "Revival"
	  , "Celtic"
	  , "Bluegrass"
	  , "Avantgarde"
	  , "Gothic Rock"
	  , "Progressive Rock"
	  , "Psychedelic Rock"
	  , "Symphonic Rock"
	  , "Slow Rock"
	  , "Big Band"
	  , "Chorus"
	  , "Easy Listening"
	  , "Acoustic"
	  , "Humour"
	  , "Speech"
	  , "Chanson"
	  , "Opera"
	  , "Chamber Music"
	  , "Sonata"
	  , "Symphony"
	  , "Booty Brass"
	  , "Primus"
	  , "Porn Groove"
	  , "Satire"
	  , "Slow Jam"
	  , "Club"
	  , "Tango"
	  , "Samba"
	  , "Folklore"
	  , "Ballad"
	  , "Power Ballad"
	  , "Rhythmic Soul"
	  , "Freestyle"
	  , "Duet"
	  , "Punk Rock"
	  , "Drum Solo"
	  , "A Capela"
	  , "Euro-House"
	  , "Dance Hall"
	  , "Goa"
	  , "Drum & Bass"
	  , "Club-House"
	  , "Hardcore"
	  , "Terror"
	  , "Indie"
	  , "BritPop"
	  , "Negerpunk"
	  , "Polsk Punk"
	  , "Beat"
	  , "Christian Gangsta Rap"
	  , "Heavy Metal"
	  , "Black Metal"
	  , "Crossover"
	  , "Contemporary Christian"
	  , "Christian Rock"
	  , "Merengue"
	  , "Salsa"
	  , "Thrash Metal"
	  , "Anime"
	  , "JPop"
	  , "SynthPop"
	  };

	public MpegAudioFileReader()
	{
		super(MARK_LIMIT, true);
		if (TDebug.TraceAudioFileReader) TDebug.out(">MpegAudioFileReader(1.9.2-FINAL)");
        try
        {
        	weak = System.getProperty("mp3spi.weak");
        }
        catch(AccessControlException e)
        {}
	}

	/**
	 * Returns AudioFileFormat from File.
	 */
	public AudioFileFormat getAudioFileFormat(File file) throws UnsupportedAudioFileException, IOException
	{		
		return super.getAudioFileFormat(file);
	}
	
	/**
	 * Returns AudioFileFormat from URL.
	 */
	public AudioFileFormat getAudioFileFormat(URL url) throws UnsupportedAudioFileException, IOException
	{		
		if (TDebug.TraceAudioFileReader) {TDebug.out("MpegAudioFileReader.getAudioFileFormat(URL): begin"); }
		long lFileLengthInBytes = AudioSystem.NOT_SPECIFIED;
		URLConnection conn = url.openConnection();
		// Tell shoucast server (if any) that SPI support shoutcast stream.
		conn.setRequestProperty ("Icy-Metadata", "1");		
		InputStream	inputStream = conn.getInputStream();
		AudioFileFormat	audioFileFormat = null;
		try
		{
			audioFileFormat = getAudioFileFormat(inputStream, lFileLengthInBytes);
		}
		finally
		{
			inputStream.close();
		}
		if (TDebug.TraceAudioFileReader) {TDebug.out("MpegAudioFileReader.getAudioFileFormat(URL): end"); }
		return audioFileFormat;
	}
	
	/**
	 * Returns AudioFileFormat from inputstream and medialength.
	 */
	public AudioFileFormat getAudioFileFormat(InputStream inputStream, long mediaLength) throws UnsupportedAudioFileException, IOException
	{		
		if (TDebug.TraceAudioFileReader) TDebug.out(">MpegAudioFileReader.getAudioFileFormat(InputStream inputStream, long mediaLength): begin");		
		HashMap aff_properties = new HashMap();
		HashMap af_properties = new HashMap();
		int mLength = (int) mediaLength;
		int size = inputStream.available();		
		PushbackInputStream pis = new PushbackInputStream(inputStream, MARK_LIMIT);
		byte head[] = new byte[12];
		pis.read(head);
		if (TDebug.TraceAudioFileReader)
		{
			TDebug.out("InputStream : "+inputStream + " =>" + new String(head));
		}
		/*
		 * Check for WAV, AU, and AIFF file formats.
		 *
		 * Next check for Shoutcast (supported) and OGG (unsupported) streams.
		 *
		 * Note -- the check for WAV files will reject Broadcast WAV files.
		 * This may be incorrect as broadcast WAV files may contain MPEG data.
		 * Need to investigate.
		 *
		 */
		if ((head[0] == 'R') && (head[1] == 'I') && (head[2] == 'F') && (head[3] == 'F') && (head[8] == 'W') && (head[9] == 'A') && (head[10] == 'V') && (head[11] == 'E'))
		{
			if (TDebug.TraceAudioFileReader) TDebug.out("WAV stream found");
			if (weak == null) throw new UnsupportedAudioFileException("WAV stream found");
		}
		else if ((head[0] == '.') && (head[1] == 's') && (head[2] == 'n') && (head[3] == 'd'))
		{
			if (TDebug.TraceAudioFileReader) TDebug.out("AU stream found");
			if (weak == null) throw new UnsupportedAudioFileException("AU stream found");
		}
		else if ((head[0] == 'F') && (head[1] == 'O') && (head[2] == 'R') && (head[3] == 'M') && (head[8] == 'A') && (head[9] == 'I') && (head[10] == 'F') && (head[11] == 'F'))
		{
			if (TDebug.TraceAudioFileReader) TDebug.out("AIFF stream found");
			if (weak == null) throw new UnsupportedAudioFileException("AIFF stream found");
		}
		// Shoutcast stream ?
		else if (((head[0] == 'I') | (head[0] == 'i')) && ((head[1] == 'C') | (head[1] == 'c')) && ((head[2] == 'Y') | (head[2] == 'y')))
		{
			pis.unread(head);
			// Load shoutcast meta data.
			loadShoutcastInfo(pis, aff_properties);			
		}
		// Ogg stream ?
		else if (((head[0] == 'O') | (head[0] == 'o')) && ((head[1] == 'G') | (head[1] == 'g')) && ((head[2] == 'G') | (head[2] == 'g')))
		{
			if (TDebug.TraceAudioFileReader) TDebug.out("Ogg stream found");
			if (weak == null) throw new UnsupportedAudioFileException("Ogg stream found");
		}
		// No, so pushback.
		else
		{
			pis.unread(head);
		}
		// MPEG header info.
		int nVersion = AudioSystem.NOT_SPECIFIED;
		int nLayer = AudioSystem.NOT_SPECIFIED;
		int nSFIndex = AudioSystem.NOT_SPECIFIED;
		int nMode = AudioSystem.NOT_SPECIFIED;
		int FrameSize = AudioSystem.NOT_SPECIFIED;
		int nFrameSize = AudioSystem.NOT_SPECIFIED;
		int nFrequency = AudioSystem.NOT_SPECIFIED;
		int nTotalFrames = AudioSystem.NOT_SPECIFIED;
		float FrameRate = AudioSystem.NOT_SPECIFIED;
		int BitRate = AudioSystem.NOT_SPECIFIED;
		int nChannels = AudioSystem.NOT_SPECIFIED;
		int nHeader = AudioSystem.NOT_SPECIFIED;
		int nTotalMS = AudioSystem.NOT_SPECIFIED;
		boolean nVBR = false;
		AudioFormat.Encoding encoding = null;
		try
		{
			Bitstream m_bitstream = new Bitstream(pis);
			aff_properties.put("mp3.header.pos",new Integer(m_bitstream.header_pos()));
			Header m_header = m_bitstream.readFrame();
			// nVersion = 0 => MPEG2-LSF (Including MPEG2.5), nVersion = 1 => MPEG1
			nVersion = m_header.version();
			if (nVersion == 2) aff_properties.put("mp3.version.mpeg",Float.toString(2.5f));			
			else aff_properties.put("mp3.version.mpeg",Integer.toString(2-nVersion));			
			// nLayer = 1,2,3
			nLayer = m_header.layer();
			aff_properties.put("mp3.version.layer",Integer.toString(nLayer));			
			nSFIndex = m_header.sample_frequency();
			nMode = m_header.mode();
			aff_properties.put("mp3.mode",new Integer(nMode));
			nChannels = nMode == 3 ? 1 : 2;
			aff_properties.put("mp3.channels",new Integer(nChannels));
			nVBR = m_header.vbr();			
			af_properties.put("vbr",new Boolean(nVBR));
			aff_properties.put("mp3.vbr",new Boolean(nVBR));
			aff_properties.put("mp3.vbr.scale",new Integer(m_header.vbr_scale()));
			FrameSize = m_header.calculate_framesize();
			aff_properties.put("mp3.framesize.bytes",new Integer(FrameSize));
			if (FrameSize < 0) throw new UnsupportedAudioFileException("Invalid FrameSize : " + FrameSize);
			nFrequency = m_header.frequency();
			aff_properties.put("mp3.frequency.hz",new Integer(nFrequency));
			FrameRate = (float) ((1.0 / (m_header.ms_per_frame())) * 1000.0);
			aff_properties.put("mp3.framerate.fps",new Float(FrameRate));
			if (FrameRate < 0) throw new UnsupportedAudioFileException("Invalid FrameRate : " + FrameRate);
			if (mLength != AudioSystem.NOT_SPECIFIED)
			{
				aff_properties.put("mp3.length.bytes",new Integer(mLength));
				nTotalFrames = m_header.max_number_of_frames(mLength);
				aff_properties.put("mp3.length.frames",new Integer(nTotalFrames));
			}
			BitRate = m_header.bitrate();			
			af_properties.put("bitrate",new Integer(BitRate));
			aff_properties.put("mp3.bitrate.nominal.bps",new Integer(BitRate));
			nHeader = m_header.getSyncHeader();
			encoding = sm_aEncodings[nVersion][nLayer - 1];
			aff_properties.put("mp3.version.encoding",encoding.toString());
			if (mLength != AudioSystem.NOT_SPECIFIED)
			{
				nTotalMS = Math.round(m_header.total_ms(mLength));
				aff_properties.put("duration",new Long((long)nTotalMS*1000L));
			}
			aff_properties.put("mp3.copyright",new Boolean(m_header.copyright()));
			aff_properties.put("mp3.original",new Boolean(m_header.original()));
			aff_properties.put("mp3.crc",new Boolean(m_header.checksums()));
			aff_properties.put("mp3.padding",new Boolean(m_header.padding()));
			InputStream id3v2 = m_bitstream.getRawID3v2();
			if (id3v2 != null)
			{
				aff_properties.put("mp3.id3tag.v2",id3v2);
				parseID3v2Frames(id3v2,aff_properties);
			} 
			if (TDebug.TraceAudioFileReader) TDebug.out(m_header.toString());
		}
		catch (Exception e)
		{
			if (TDebug.TraceAudioFileReader) TDebug.out("not a MPEG stream:" + e.getMessage());
			throw new UnsupportedAudioFileException("not a MPEG stream:" + e.getMessage());
		}

		// Deeper checks ?
		int cVersion = (nHeader >> 19) & 0x3;
		if (cVersion == 1)
		{
			if (TDebug.TraceAudioFileReader) TDebug.out("not a MPEG stream: wrong version");
			throw new UnsupportedAudioFileException("not a MPEG stream: wrong version");
		}

		int cSFIndex = (nHeader >> 10) & 0x3;
		if (cSFIndex == 3)
		{
			if (TDebug.TraceAudioFileReader) TDebug.out("not a MPEG stream: wrong sampling rate");
			throw new UnsupportedAudioFileException("not a MPEG stream: wrong sampling rate");
		}
		
		// Look up for ID3v1 tag
		if ((size == mediaLength) && (mediaLength != AudioSystem.NOT_SPECIFIED))
		{
			FileInputStream fis = (FileInputStream) inputStream;
			byte[] id3v1 = new byte[128];
			long bytesSkipped = fis.skip(inputStream.available()-id3v1.length);
			int read = fis.read(id3v1,0,id3v1.length);
			if ((id3v1[0]=='T') && (id3v1[1]=='A') && (id3v1[2]=='G'))
			{
				parseID3v1Frames(id3v1, aff_properties);
			}
		}		
		
		AudioFormat format = new MpegAudioFormat(encoding, (float) nFrequency, AudioSystem.NOT_SPECIFIED // SampleSizeInBits - The size of a sample
		, nChannels // Channels - The number of channels
		, -1 // The number of bytes in each frame
		, FrameRate // FrameRate - The number of frames played or recorded per second
		, true
		, af_properties);
		return new MpegAudioFileFormat(MpegFileFormatType.MP3, format, nTotalFrames, mLength,aff_properties);
	}

	/**
	 * Returns AudioInputStream from file.
	 */
	public AudioInputStream getAudioInputStream(File file) throws UnsupportedAudioFileException, IOException
	{
	  if (TDebug.TraceAudioFileReader) TDebug.out("getAudioInputStream(File file)");	  
	  InputStream inputStream = new FileInputStream(file);
	  try
	  {		
		return getAudioInputStream(inputStream);
	  }
	  catch (UnsupportedAudioFileException e)
	  {
		if (inputStream != null) inputStream.close();
		throw e;
	  }
	  catch (IOException e)
	  {
		if (inputStream != null) inputStream.close();
		throw e;
	  }
	}

	/**
	 * Returns AudioInputStream from url.
	 */
	public AudioInputStream getAudioInputStream(URL url)
		throws	UnsupportedAudioFileException, IOException
	{
		if (TDebug.TraceAudioFileReader) {TDebug.out("MpegAudioFileReader.getAudioInputStream(URL): begin"); }
		long lFileLengthInBytes = AudioSystem.NOT_SPECIFIED;
		URLConnection conn = url.openConnection();
		// Tell shoucast server (if any) that SPI support shoutcast stream.
		boolean isShout = false;
		int toRead=4;
		byte[] head = new byte[toRead];
		
		conn.setRequestProperty ("Icy-Metadata", "1");
		BufferedInputStream bInputStream = new BufferedInputStream(conn.getInputStream());
		bInputStream.mark(toRead);
		int read = bInputStream.read(head,0,toRead);	
		if ((read>2) && (((head[0] == 'I') | (head[0] == 'i')) && ((head[1] == 'C') | (head[1] == 'c')) && ((head[2] == 'Y') | (head[2] == 'y')))) isShout = true;
		bInputStream.reset();
		InputStream	inputStream = null;
		// Is is a shoutcast server ?
		if (isShout == true)
		{
			// Yes
			IcyInputStream icyStream = new IcyInputStream(bInputStream);
			icyStream.addTagParseListener(IcyListener.getInstance());
			inputStream = icyStream;	
		}
		else
		{
			// No, is Icecast 2 ?
			String metaint = conn.getHeaderField("icy-metaint");
			if (metaint != null)
			{
				// Yes, it might be icecast 2 mp3 stream.
				IcyInputStream icyStream = new IcyInputStream(bInputStream,metaint);
				icyStream.addTagParseListener(IcyListener.getInstance());
				inputStream = icyStream;					
			}
			else
			{
				// No
				inputStream = bInputStream;
			}					
		} 
		AudioInputStream audioInputStream = null;
		try
		{
			audioInputStream = getAudioInputStream(inputStream, lFileLengthInBytes);
		}
		catch (UnsupportedAudioFileException e)
		{
			inputStream.close();
			throw e;
		}
		catch (IOException e)
		{
			inputStream.close();
			throw e;
		}
		if (TDebug.TraceAudioFileReader) {TDebug.out("MpegAudioFileReader.getAudioInputStream(URL): end"); }
		return audioInputStream;
	}

	/**
	 * Return the AudioInputStream from the given InputStream.
	 */
	public AudioInputStream getAudioInputStream(InputStream inputStream) throws UnsupportedAudioFileException, IOException
	{
	  if (TDebug.TraceAudioFileReader) TDebug.out("MpegAudioFileReader.getAudioInputStream(InputStream inputStream)");
	  if (!inputStream.markSupported()) inputStream = new BufferedInputStream(inputStream);		  
	  return super.getAudioInputStream(inputStream);
	}
	
	/**
	 * Parser ID3v1 frames
	 * @param frames
	 * @param props
	 */
	protected void parseID3v1Frames(byte[] frames, HashMap props)
	{
		if (TDebug.TraceAudioFileReader) TDebug.out("Parsing ID3v1");
		String tag = null;
		try
		{
			tag = new String(frames, 0, frames.length, "ISO-8859-1"); 
		}
		catch (UnsupportedEncodingException e)
		{
			tag = new String(frames, 0, frames.length);
			if (TDebug.TraceAudioFileReader) TDebug.out("Cannot use ISO-8859-1");
		}
		if (TDebug.TraceAudioFileReader) TDebug.out("ID3v1 frame dump='"+tag+"'");
		int start = 3;
		String titlev1 = chopSubstring(tag, start, start += 30);
		String titlev2 = (String) props.get("title"); 
		if (((titlev2==null) || (titlev2.length()==0)) && (titlev1 != null)) props.put("title",titlev1);
		String artistv1 = chopSubstring(tag, start, start += 30);
		String artistv2 = (String) props.get("author"); 
		if (((artistv2==null) || (artistv2.length()==0)) && (artistv1 != null)) props.put("author",artistv1);				
		String albumv1 = chopSubstring(tag, start, start += 30);
		String albumv2 = (String) props.get("album");
		if (((albumv2==null) || (albumv2.length()==0)) && (albumv1 != null)) props.put("album",albumv1);
		String yearv1 = chopSubstring(tag, start, start += 4);
		String yearv2 = (String) props.get("year");
		if (((yearv2==null) || (yearv2.length()==0)) && (yearv1 != null)) props.put("date",yearv1);	
		String commentv1 = chopSubstring(tag, start, start += 28);
		String commentv2 = (String) props.get("comment");
		if (((commentv2==null) || (commentv2.length()==0)) && (commentv1 != null)) props.put("comment",commentv1);		
		String trackv1 = ""+((int) (frames[126] & 0xff));
		String trackv2 = (String) props.get("mp3.id3tag.track");
		if (((trackv2==null) || (trackv2.length()==0)) && (trackv1 != null)) props.put("mp3.id3tag.track",trackv1);
		
		int genrev1 = (int) (frames[127] & 0xff);
		if ((genrev1 >=0) && (genrev1<id3v1genres.length))
		{
			String genrev2 = (String) props.get("mp3.id3tag.genre");
			if (((genrev2==null) || (genrev2.length()==0))) props.put("mp3.id3tag.genre",id3v1genres[genrev1]);	
		}
		if (TDebug.TraceAudioFileReader) TDebug.out("ID3v1 parsed");		
	}
	
	/**
	 * Extract 
	 * @param s
	 * @param start
	 * @param end
	 * @return
	 */
	private String chopSubstring(String s, int start, int end) 
	{
		String str = null;
		// 11/28/04 - String encoding bug fix.
		try
		{
			str = s.substring(start, end);
			int loc = str.indexOf('\0');  	
			if (loc != -1) str = str.substring(0, loc);
		}
		catch (StringIndexOutOfBoundsException e)
		{
			// Skip encoding issues.
			if (TDebug.TraceAudioFileReader) TDebug.out("Cannot chopSubString "+e.getMessage());
		}
		return str;
  	}
	/**
	 * Parse ID3v2 frames to add album (TALB), title (TIT2), date (TYER), author (TPE1), copyright (TCOP), comment (COMM) ...
	 * @param frames
	 * @param props
	 */
	protected void parseID3v2Frames(InputStream frames, HashMap props)
	{
		if (TDebug.TraceAudioFileReader) TDebug.out("Parsing ID3v2");
		byte[] bframes = null;
		int size = -1;
		try
		{
			size = frames.available();
			bframes = new byte[size];
			frames.mark(size);					
			frames.read(bframes);
			frames.reset();					
		}
		catch (IOException e)
		{
			if (TDebug.TraceAudioFileReader) TDebug.out("Cannot parse ID3v2 :"+e.getMessage());
		}

		try
		{
			if (TDebug.TraceAudioFileReader) TDebug.out("ID3v2 frame dump='"+new String(bframes,0,bframes.length)+"'");
			/* ID3 tags : http://www.unixgods.org/~tilo/ID3/docs/ID3_comparison.html */
			String value = null;
			for (int i=0;i<bframes.length-4;i++)
			{
				String code = new String(bframes,i,4);
				String scode = new String(bframes,i,3);
				// ID3v2.3 & v2.4
				if ((code.equals("TALB")) || (code.equals("TIT2")) || (code.equals("TYER")) || (code.equals("TPE1")) || (code.equals("TCOP")) || (code.equals("COMM")) || (code.equals("TCON")) || (code.equals("TRCK")) || (code.equals("TPOS")) || (code.equals("TDRC")) || (code.equals("TCOM")) || (code.equals("TIT1")) || (code.equals("TENC")))
				{
					i=i+10;
					size = (int) (bframes[i-6] << 24) + (bframes[i-5] << 16) + (bframes[i-4] << 8) + (bframes[i-3]);					
					if (code.equals("COMM")) value = parseText(bframes, i, size, 5);
					else value = parseText(bframes,i, size, 1); 					
					if ((value != null) && (value.length()>0))
					{
						if (code.equals("TALB")) props.put("album",value);
						else if (code.equals("TIT2")) props.put("title",value);
						else if (code.equals("TYER")) props.put("date",value);
						// ID3v2.4 date fix.
						else if (code.equals("TDRC")) props.put("date",value);
						else if (code.equals("TPE1")) props.put("author",value);
						else if (code.equals("TCOP")) props.put("copyright",value);
						else if (code.equals("COMM")) props.put("comment",value);						
						else if (code.equals("TCON")) props.put("mp3.id3tag.genre",value);
						else if (code.equals("TRCK")) props.put("mp3.id3tag.track",value);
						else if (code.equals("TPOS")) props.put("mp3.id3tag.disc",value);
						else if (code.equals("TCOM")) props.put("mp3.id3tag.composer",value);
						else if (code.equals("TIT1")) props.put("mp3.id3tag.grouping",value);
						else if (code.equals("TENC")) props.put("mp3.id3tag.encoded",value);
					} 
					i=i+size-1;
				}
				// ID3v2.2.
				else if ((scode.equals("TAL")) || (scode.equals("TT2")) || (scode.equals("TP1")) || (scode.equals("TYE")) || (scode.equals("TRK")) || (scode.equals("TPA")) || (scode.equals("TCR")) || (scode.equals("TCO")) || (scode.equals("TCM")) || (scode.equals("COM")) || (scode.equals("TT1")) || (scode.equals("TEN")))
				{
					i=i+6;
					size = (int) (0x00000000) + (bframes[i-3] << 16) + (bframes[i-2] << 8) + (bframes[i-1]);					
					if (scode.equals("COM")) value = parseText(bframes, i, size, 5);
					else value = parseText(bframes,i, size, 1); 					
					if ((value != null) && (value.length()>0))
					{
						if (scode.equals("TAL")) props.put("album",value);
						else if (scode.equals("TT2")) props.put("title",value);
						else if (scode.equals("TYE")) props.put("date",value);
						else if (scode.equals("TP1")) props.put("author",value);
						else if (scode.equals("TCR")) props.put("copyright",value);
						else if (scode.equals("COM")) props.put("comment",value);						
						else if (scode.equals("TCO")) props.put("mp3.id3tag.genre",value);
						else if (scode.equals("TRK")) props.put("mp3.id3tag.track",value);
						else if (scode.equals("TPA")) props.put("mp3.id3tag.disc",value);
						else if (scode.equals("TCM")) props.put("mp3.id3tag.composer",value);
						else if (scode.equals("TT1")) props.put("mp3.id3tag.grouping",value);
						else if (scode.equals("TEN")) props.put("mp3.id3tag.encoded",value);
					} 
					i=i+size-1;
				}
				else if (code.startsWith("ID3"))
				{
					// ID3v2 Header.
					int v2version = (int) (bframes[3] & 0xFF);
					props.put("mp3.id3tag.v2.version",String.valueOf(v2version));
					i=i+4;
				}
			}
		}
		catch (RuntimeException e)
		{
			// Ignore all parsing errors.
			if (TDebug.TraceAudioFileReader) TDebug.out("Cannot parse ID3v2 :"+e.getMessage());
		}
		if (TDebug.TraceAudioFileReader) TDebug.out("ID3v2 parsed");
	}

	/**
	 * Parse Text Frames.
	 * @param bframes
	 * @param offset
	 * @param size
	 * @param skip
	 * @return
	 */
	protected String parseText(byte[] bframes, int offset, int size, int skip)
	{
		String value = null;
		try
		{
			String[] ENC_TYPES = {"ISO-8859-1", "UTF16","UTF-16BE", "UTF-8"};
			value = new String(bframes,offset+skip,size-skip,ENC_TYPES[bframes[offset]]);
			value = chopSubstring(value,0,value.length());				
		}
		catch (UnsupportedEncodingException e)
		{
			if (TDebug.TraceAudioFileReader) TDebug.out("ID3v2 Encoding error :"+e.getMessage());
		}
		return value;
	}
	
	/**
	 * Load shoutcast (ICY) info.
	 * @param input
	 * @param props
	 * @throws IOException
	 */
	protected void loadShoutcastInfo(InputStream input, HashMap props) throws IOException
	{
	  IcyInputStream icy = new IcyInputStream(new BufferedInputStream(input));
	  HashMap metadata = icy.getTagHash();
	  MP3Tag titleMP3Tag = icy.getTag("icy-name");
	  if (titleMP3Tag != null) props.put("title",((String) titleMP3Tag.getValue()).trim());
	  MP3Tag[] meta = icy.getTags();
	  if (meta != null)
	  {
		  StringBuffer metaStr = new StringBuffer();
		  for (int i=0;i<meta.length;i++)
		  {
			  String key = meta[i].getName();
			  String value = ((String) icy.getTag(key).getValue()).trim();
			  props.put("mp3.shoutcast.metadata."+key, value);	  				  
		  }
	  }
	}
	
}