package org.red5.io.mp3.impl;

/*
 * RED5 Open Source Flash Server - http://www.osflash.org/red5
 * 
 * Copyright (c) 2006-2007 by respective authors (see below). All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or modify it under the 
 * terms of the GNU Lesser General Public License as published by the Free Software 
 * Foundation; either version 2.1 of the License, or (at your option) any later 
 * version. 
 * 
 * This library 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along 
 * with this library; if not, write to the Free Software Foundation, Inc., 
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.mina.common.ByteBuffer;
import org.red5.io.IKeyFrameMetaCache;
import org.red5.io.IStreamableFile;
import org.red5.io.ITag;
import org.red5.io.ITagReader;
import org.red5.io.IoConstants;
import org.red5.io.amf.Output;
import org.red5.io.flv.IKeyFrameDataAnalyzer;
import org.red5.io.flv.impl.Tag;
import org.red5.io.object.Serializer;

/**
 * Read MP3 files
 */
public class MP3Reader implements ITagReader, IKeyFrameDataAnalyzer {
    /**
     * Logger
     */
	protected static Log log = LogFactory.getLog(MP3Reader.class.getName());

    /**
     * File
     */
    private File file;
    /**
     * File input stream
     */
    private FileInputStream fis;
    /**
     * File channel
     */
	private FileChannel channel;
    /**
     * Memory-mapped buffer for file content
     */
	private MappedByteBuffer mappedFile;
    /**
     * Source byte buffer
     */
	private ByteBuffer in;
    /**
     * Last read tag object
     */
	private ITag tag;
    /**
     * Previous tag size
     */
	private int prevSize;
    /**
     * Current time
     */
	private double currentTime;
    /**
     * Frame metadata
     */
	private KeyFrameMeta frameMeta;
    /**
     * Positions and time map
     */
	private HashMap<Integer, Double> posTimeMap;

	private int dataRate;
    /**
     * Whether first frame is read
     */
	private boolean firstFrame;
    /**
     * File metadata
     */
	private ITag fileMeta;
    /**
     * File duration
     */
	private long duration;
    /**
     * Frame cache
     */
	static private IKeyFrameMetaCache frameCache;

	MP3Reader() {
		// Only used by the bean startup code to initialize the frame cache
	}
	
    /**
     * Creates reader from file input stream
     * @param stream          File input stream source
     */
    public MP3Reader(File file) throws FileNotFoundException {
    	this.file = file;
		fis = new FileInputStream(file);
        // Grab file channel and map it to memory-mapped byte buffer in read-only mode
        channel = fis.getChannel();
		try {
			mappedFile = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel
					.size());
		} catch (IOException e) {
			log.error("MP3Reader :: MP3Reader ::>\n", e);
		}

        // Use Big Endian bytes order
        mappedFile.order(ByteOrder.BIG_ENDIAN);
        // Wrap mapped byte buffer to MINA buffer
        in = ByteBuffer.wrap(mappedFile);
        // Analyze keyframes data
        analyzeKeyFrames();
		firstFrame = true;
		
        // Process ID3v2 header if present
		processID3v2Header();
		
        // Create file metadata object
        fileMeta = createFileMeta();

        // MP3 header is length of 32 bits, that is, 4 bytes
        // Read further if there's still data
        if (in.remaining() > 4) {
            // Look to next frame
            searchNextFrame();
            // Set position
            int pos = in.position();
            // Read header...
            // Data in MP3 file goes header-data-header-data...header-data
            MP3Header header = readHeader();
            // Set position
            in.position(pos);
            // Check header
            if (header != null) {
				checkValidHeader(header);
			} else {
				throw new RuntimeException("No initial header found.");
			}
		}
	}

    /**
     * A MP3 stream never has video.
     * 
     * @return always returns <code>false</code>
     */
    public boolean hasVideo() {
    	return false;
    }
    
    public void setFrameCache(IKeyFrameMetaCache frameCache) {
    	MP3Reader.frameCache = frameCache;
    }
    
	/**
	 * Check if the file can be played back with Flash. Supported sample rates are
     * 44KHz, 22KHz, 11KHz and 5.5KHz
     * 
	 * @param header       Header to check
	 */
	private void checkValidHeader(MP3Header header) {
		switch (header.getSampleRate()) {
			case 44100:
			case 22050:
			case 11025:
			case 5513:
				// Supported sample rate
				break;

			default:
				throw new RuntimeException("Unsupported sample rate: "
						+ header.getSampleRate());
		}
	}

    /**
     * Creates file metadata object
     * @return         Tag
     */
    private ITag createFileMeta() {
		// Create tag for onMetaData event
		ByteBuffer buf = ByteBuffer.allocate(1024);
		buf.setAutoExpand(true);
		Output out = new Output(buf);
		out.writeString("onMetaData");
		Map<Object, Object> props = new HashMap<Object, Object>();
		props.put("duration", frameMeta.timestamps[frameMeta.timestamps.length - 1] / 1000.0);
		props.put("audiocodecid", IoConstants.FLAG_FORMAT_MP3);
		if (dataRate > 0) {
			props.put("audiodatarate", dataRate);
		}
		props.put("canSeekToEnd", true);
		out.writeMap(props, new Serializer());
		buf.flip();

		ITag result = new Tag(IoConstants.TYPE_METADATA, 0, buf.limit(), null,
				prevSize);
		result.setBody(buf);
		return result;
	}

	/** Search for next frame sync word. Sync word identifies valid frame. */
	public void searchNextFrame() {
		while (in.remaining() > 1) {
			int ch = in.get() & 0xff;
			if (ch != 0xff) {
				continue;
			}

			if ((in.get() & 0xe0) == 0xe0) {
				// Found it
				in.position(in.position() - 2);
				return;
			}
		}
	}

	/** {@inheritDoc} */
    public IStreamableFile getFile() {
		// TODO Auto-generated method stub
		return null;
	}

	/** {@inheritDoc} */
    public int getOffset() {
		// TODO Auto-generated method stub
		return 0;
	}

	/** {@inheritDoc} */
    public long getBytesRead() {
		return in.position();
	}

	/** {@inheritDoc} */
    public long getDuration() {
		return duration;
	}

	/** {@inheritDoc} */
    public boolean hasMoreTags() {
		MP3Header header = null;
		while (header == null && in.remaining() > 4) {
			try {
				header = new MP3Header(in.getInt());
			} catch (IOException e) {
				log.error("MP3Reader :: hasMoreTags ::>\n", e);
				break;
			} catch (Exception e) {
				searchNextFrame();
			}
		}

		if (header == null) {
			return false;
		}

		if (header.frameSize() == 0) {
			// TODO find better solution how to deal with broken files...
			// See APPSERVER-62 for details
			return false;
		}
		
		if (in.position() + header.frameSize() - 4 > in.limit()) {
			// Last frame is incomplete
			in.position(in.limit());
			return false;
		}

		in.position(in.position() - 4);
		return true;
	}

	private MP3Header readHeader() {
		MP3Header header = null;
		while (header == null && in.remaining() > 4) {
			try {
				header = new MP3Header(in.getInt());
			} catch (IOException e) {
				log.error("MP3Reader :: readTag ::>\n", e);
				break;
			} catch (Exception e) {
				searchNextFrame();
			}
		}
		return header;
	}

	/** {@inheritDoc} */
    public synchronized ITag readTag() {
		if (firstFrame) {
			// Return file metadata as first tag.
			firstFrame = false;
			return fileMeta;
		}

		MP3Header header = readHeader();
		if (header == null) {
			return null;
		}

		int frameSize = header.frameSize();
		if (frameSize == 0) {
			// TODO find better solution how to deal with broken files...
			// See APPSERVER-62 for details
			return null;
		}
		
		if (in.position() + frameSize - 4 > in.limit()) {
			// Last frame is incomplete
			in.position(in.limit());
			return null;
		}

		tag = new Tag(IoConstants.TYPE_AUDIO, (int) currentTime, frameSize + 1,
				null, prevSize);
		prevSize = frameSize + 1;
		currentTime += header.frameDuration();
		ByteBuffer body = ByteBuffer.allocate(tag.getBodySize());
		byte tagType = (IoConstants.FLAG_FORMAT_MP3 << 4)
				| (IoConstants.FLAG_SIZE_16_BIT << 1);
		switch (header.getSampleRate()) {
			case 44100:
				tagType |= IoConstants.FLAG_RATE_44_KHZ << 2;
				break;
			case 22050:
				tagType |= IoConstants.FLAG_RATE_22_KHZ << 2;
				break;
			case 11025:
				tagType |= IoConstants.FLAG_RATE_11_KHZ << 2;
				break;
			default:
				tagType |= IoConstants.FLAG_RATE_5_5_KHZ << 2;
		}
		tagType |= (header.isStereo() ? IoConstants.FLAG_TYPE_STEREO
				: IoConstants.FLAG_TYPE_MONO);
		body.put(tagType);
		final int limit = in.limit();
		body.putInt(header.getData());
		in.limit(in.position() + frameSize - 4);
		body.put(in);
		body.flip();
		in.limit(limit);

		tag.setBody(body);

		return tag;
	}

	/** {@inheritDoc} */
    public void close() {
		if (posTimeMap != null) {
			posTimeMap.clear();
		}
		mappedFile.clear();
		if (in != null) {
			in.release();
			in = null;
		}
		try {
			fis.close();
			channel.close();
		} catch (IOException e) {
			log.error("MP3Reader :: close ::>\n", e);
		}
	}

	/** {@inheritDoc} */
    public void decodeHeader() {
	}

	/** {@inheritDoc} */
    public void position(long pos) {
    	if (pos == Long.MAX_VALUE) {
    		// Seek at EOF
    		in.position(in.limit());
    		currentTime = duration;
    		return;
    	}
		in.position((int) pos);
		// Advance to next frame
		searchNextFrame();
		// Make sure we can resolve file positions to timestamps
		analyzeKeyFrames();
		Double time = posTimeMap.get(in.position());
		if (time != null) {
			currentTime = time;
		} else {
			// Unknown frame position - this should never happen
			currentTime = 0;
		}
	}

	/** {@inheritDoc} */
    public synchronized KeyFrameMeta analyzeKeyFrames() {
		if (frameMeta != null) {
			return frameMeta;
		}

		// check for cached frame informations
		if (frameCache != null) {
			frameMeta = frameCache.loadKeyFrameMeta(file);
			if (frameMeta != null && frameMeta.duration > 0) {
				// Frame data loaded, create other mappings
				duration = frameMeta.duration;
				frameMeta.audioOnly = true;
				posTimeMap = new HashMap<Integer, Double>();
				for (int i=0; i<frameMeta.positions.length; i++) {
					posTimeMap.put((int) frameMeta.positions[i], (double) frameMeta.timestamps[i]);
				}
				return frameMeta;
			}
		}

		List<Integer> positionList = new ArrayList<Integer>();
		List<Double> timestampList = new ArrayList<Double>();
		dataRate = 0;
		long rate = 0;
		int count = 0;
		int origPos = in.position();
		double time = 0;
		in.position(0);
		processID3v2Header();
		searchNextFrame();
		while (this.hasMoreTags()) {
			MP3Header header = readHeader();
			if (header == null) {
				// No more tags
				break;
			}

			if (header.frameSize() == 0) {
				// TODO find better solution how to deal with broken files...
				// See APPSERVER-62 for details
				break;
			}
			
			int pos = in.position() - 4;
			if (pos + header.frameSize() > in.limit()) {
				// Last frame is incomplete
				break;
			}

			positionList.add(pos);
			timestampList.add(time);
			rate += header.getBitRate() / 1000;
			time += header.frameDuration();
			in.position(pos + header.frameSize());
			count++;
		}
		// restore the pos
		in.position(origPos);

		duration = (long) time;
		dataRate = (int) (rate / count);
		posTimeMap = new HashMap<Integer, Double>();
		frameMeta = new KeyFrameMeta();
		frameMeta.duration = duration;
		frameMeta.positions = new long[positionList.size()];
		frameMeta.timestamps = new int[timestampList.size()];
		frameMeta.audioOnly = true;
		for (int i = 0; i < frameMeta.positions.length; i++) {
			frameMeta.positions[i] = positionList.get(i);
			frameMeta.timestamps[i] = timestampList.get(i).intValue();
			posTimeMap.put(positionList.get(i), timestampList.get(i));
		}
		if (frameCache != null)
			frameCache.saveKeyFrameMeta(file, frameMeta);
		
		return frameMeta;
	}

    private void processID3v2Header() {
    	if (in.remaining() <= 10)
    		// We need at least 10 bytes ID3v2 header + data
    		return;
    	
    	int start = in.position();
    	byte a, b, c;
    	a = in.get();
    	b = in.get();
    	c = in.get();
    	if (a != 'I' || b != 'D' || c != '3') {
    		// No ID3v2 header
    		in.position(start);
    		return;
    	}
    	
    	// Skip version and flags
    	in.skip(3);
    	int size = (in.get() & 0x7f) << 21 | (in.get() & 0x7f) << 14 | (in.get() & 0x7f) << 7 | (in.get() & 0x7f);
    	// Skip ID3v2 header for now
    	in.skip(size);
    }
    
}
