/*
 * MpegAudioFileReader.
 *
 * 10/10/05 : size computation bug fixed in parseID3v2Frames.
 *            RIFF/MP3 header support added.
 *            FLAC and MAC headers throw UnsupportedAudioFileException now.
 *            "mp3.id3tag.publisher" (TPUB/TPB) added.
 *            "mp3.id3tag.orchestra" (TPE2/TP2) added.
 *            "mp3.id3tag.length" (TLEN/TLE) added.
 *
 * 08/15/05 : parseID3v2Frames improved.
 *
 * 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 {

    public static final String VERSION = "MP3SPI 1.9.5";
    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},};
    public static int INITAL_READ_LENGTH = 4 * 1024 * 1024;
    private static int MARK_LIMIT = INITAL_READ_LENGTH + 1;

    static {
        String s = System.getProperty("marklimit");
        if (s != null) {
            try {
                INITAL_READ_LENGTH = Integer.parseInt(s);
                MARK_LIMIT = INITAL_READ_LENGTH + 1;
            } catch (NumberFormatException e) {
            }
        }
    }
    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(VERSION);
        }
        try {
            weak = System.getProperty("mp3spi.weak");
        } catch (AccessControlException e) {
        }
    }

    /**
     * Returns AudioFileFormat from File.
     */
    @Override
    public AudioFileFormat getAudioFileFormat(File file) throws UnsupportedAudioFileException, IOException {
        return super.getAudioFileFormat(file);
    }

    /**
     * Returns AudioFileFormat from URL.
     */
    @Override
    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<String, Object> aff_properties = new HashMap<String, Object>();
        HashMap<String, Object> af_properties = new HashMap<String, Object>();
        int mLength = (int) mediaLength;
        int size = inputStream.available();
        PushbackInputStream pis = new PushbackInputStream(inputStream, MARK_LIMIT);
        byte head[] = new byte[22];
        pis.read(head);
        if (TDebug.TraceAudioFileReader) {
            TDebug.out("InputStream : " + inputStream + " =>" + new String(head));
        }

        // Check for WAV, AU, and AIFF, Ogg Vorbis, Flac, MAC file formats.
        // Next check for Shoutcast (supported) and OGG (unsupported) streams.
        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("RIFF/WAV stream found");
            }
            int isPCM = ((head[21] << 8) & 0x0000FF00) | ((head[20]) & 0x00000FF);
            if (weak == null) {
                if (isPCM == 1) {
                    throw new UnsupportedAudioFileException("WAV PCM 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");
            }
        } else if (((head[0] == 'M') | (head[0] == 'm')) && ((head[1] == 'A') | (head[1] == 'a')) && ((head[2] == 'C') | (head[2] == 'c'))) {
            if (TDebug.TraceAudioFileReader) {
                TDebug.out("APE stream found");
            }
            if (weak == null) {
                throw new UnsupportedAudioFileException("APE stream found");
            }
        } else if (((head[0] == 'F') | (head[0] == 'f')) && ((head[1] == 'L') | (head[1] == 'l')) && ((head[2] == 'A') | (head[2] == 'a')) && ((head[3] == 'C') | (head[3] == 'c'))) {
            if (TDebug.TraceAudioFileReader) {
                TDebug.out("FLAC stream found");
            }
            if (weak == null) {
                throw new UnsupportedAudioFileException("FLAC 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);
            int streamPos = m_bitstream.header_pos();
            aff_properties.put("mp3.header.pos", new Integer(streamPos));
            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", Boolean.valueOf(nVBR));
            aff_properties.put("mp3.vbr", Boolean.valueOf(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);
            }
            // Remove heading tag length from real stream length.
            int tmpLength = mLength;
            if ((streamPos > 0) && (mLength != AudioSystem.NOT_SPECIFIED) && (streamPos < mLength)) {
                tmpLength = tmpLength - streamPos;
            }
            if (mLength != AudioSystem.NOT_SPECIFIED) {
                aff_properties.put("mp3.length.bytes", new Integer(mLength));
                nTotalFrames = m_header.max_number_of_frames(tmpLength);
                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(tmpLength));
                aff_properties.put("duration", new Long((long) nTotalMS * 1000L));
            }
            aff_properties.put("mp3.copyright", Boolean.valueOf(m_header.copyright()));
            aff_properties.put("mp3.original", Boolean.valueOf(m_header.original()));
            aff_properties.put("mp3.crc", Boolean.valueOf(m_header.checksums()));
            aff_properties.put("mp3.padding", Boolean.valueOf(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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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());
            }
        }
        if (!"ID3".equals(new String(bframes, 0, 3))) {
            TDebug.out("No ID3v2 header found!");
            return;
        }
        int v2version = (int) (bframes[3] & 0xFF);
        props.put("mp3.id3tag.v2.version", String.valueOf(v2version));
        if (v2version < 2 || v2version > 4) {
            TDebug.out("Unsupported ID3v2 version " + v2version + "!");
            return;
        }
        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 = 10; i < bframes.length && bframes[i] > 0; i += size) {
                if (v2version == 3 || v2version == 4) {
                    // ID3v2.3 & ID3v2.4
                    String code = new String(bframes, i, 4);
                    size = (int) ((bframes[i + 4] << 24) & 0xFF000000 | (bframes[i + 5] << 16) & 0x00FF0000 | (bframes[i + 6] << 8) & 0x0000FF00 | (bframes[i + 7]) & 0x000000FF);
                    i += 10;
                    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")) || (code.equals("TPUB")) || (code.equals("TPE2"))
                            || (code.equals("TLEN"))) {
                        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);
                            } else if (code.equals("TPUB")) {
                                props.put("mp3.id3tag.publisher", value);
                            } else if (code.equals("TPE2")) {
                                props.put("mp3.id3tag.orchestra", value);
                            } else if (code.equals("TLEN")) {
                                props.put("mp3.id3tag.length", value);
                            }
                        }
                    }
                } else {
                    // ID3v2.2
                    String scode = new String(bframes, i, 3);
                    size = (int) (0x00000000) + (bframes[i + 3] << 16) + (bframes[i + 4] << 8) + (bframes[i + 5]);
                    i += 6;
                    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"))
                            || (scode.equals("TPB")) || (scode.equals("TP2")) || (scode.equals("TLE"))) {
                        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);
                            } else if (scode.equals("TPB")) {
                                props.put("mp3.id3tag.publisher", value);
                            } else if (scode.equals("TP2")) {
                                props.put("mp3.id3tag.orchestra", value);
                            } else if (scode.equals("TLE")) {
                                props.put("mp3.id3tag.length", value);
                            }
                        }
                    }
                }
            }
        } 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) {
//            StringBuilder metaStr = new StringBuilder();
            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);
            }
        }
    }
}
