package dareka.processor.util;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.Socket;

import dareka.common.Config;
import dareka.common.Logger;
import dareka.processor.HttpRequestHeader;
import dareka.processor.HttpResponseHeader;
import dareka.processor.HttpUtil;
import dareka.processor.Resource;
import dareka.processor.impl.Cache;

//flv 2 mp3 converter
public class Flv2mp3 extends Resource
{
	private int fileOffset;
	private int fileLength;
	private RandomAccessFile raf = null;
	private Cache cache = null;
	private boolean writeFailed = false;
	
	// Resource.transferTo
	@Override
	public boolean transferTo(Socket receiver, HttpRequestHeader requestHeader,
			Config config) throws IOException {

		HttpResponseHeader responseHeader = new HttpResponseHeader(
				"HTTP/1.0 200 OK\r\n\r\n");
		responseHeader.setMessageHeader("Connection", "close");
		responseHeader.setMessageHeader("Content-Type", "audio/mp3");

// ダウンロード時に動画タイトルをつける
		String downloadName = cache.getId() + ".mp3";
		String userAgent = requestHeader.getMessageHeader("User-Agent");
		String charset = null;
		if (Boolean.getBoolean("useDownloadCacheName") && userAgent != null) {
			if (userAgent.contains("MSIE") || userAgent.contains("Chrome")) {
				charset = "MS932";
// Chromeだとmp3は勝手に拡張子つけるので、なしにしないとダブる
				downloadName = cache.getTitle();
				if (userAgent.contains("MSIE")) {
					downloadName += ".mp3";
				}
			} else if (!userAgent.contains("Safari")) {
				charset = "UTF-8";
				downloadName = cache.getTitle() + ".mp3";
			}
		}
		responseHeader.setMessageHeaderOnCharset("Content-Disposition",
				"attachment; filename=\"" + downloadName + "\"", charset);

		fireOnResponseHeader(responseHeader);
		HttpUtil.sendHeader(receiver, responseHeader);

		extractMP3(receiver.getOutputStream());
		// サイズが分からないのでchunkedを使わない限りkeep-alive不可能
		return false;
	}
	
	// コンストラクタ
	public Flv2mp3(Cache cache)
	{
		this.cache = cache;
	}
	
	// ファイルを開いて処理を開始する
	public void extractMP3(OutputStream out)
	{
		DataOutputStream dout = new DataOutputStream(out);
		try {
			raf = new RandomAccessFile(cache.getCacheFile(), "r");
			fileLength = (int)cache.getCacheFile().length();
			extractMP3impl(dout);
		} catch (FileNotFoundException e) {
		} catch (EOFException e) {
			Logger.warning("Unexpected EOF.");
		}catch (IOException e) {
			Logger.warning("Failed to extract mp3.");
		} finally {
			try {
				if (raf != null)
					raf.close();
			} catch (IOException e) { }
		}
	}
	
	// ID3ヘッダを追加する
	private void addID3URL(String desc, String data, DataOutputStream id3)
	{
		String tag = "WXXX";
		try {
			byte[] uDesc = desc.getBytes("ISO-8859-1");
			byte[] uData = data.getBytes("ISO-8859-1");
			id3.write(tag.getBytes("ISO-8859-1"));
			id3.writeInt(uData.length + uDesc.length + 3);
			id3.writeShort(0);
			id3.write(0);		// encoding
			id3.write(uDesc);	// string
			id3.write(0);		// null-string
			id3.write(uData);	// string
			id3.write(0);		// null-string
		} catch (UnsupportedEncodingException e) {
		} catch (IOException e) {
		}
	}
	private void addID3Comment(String desc, String data, DataOutputStream id3)
	{
		String tag = "COMM";
		try {
			byte[] uDesc = desc.getBytes("UTF-16LE");
			byte[] uData = data.getBytes("UTF-16LE");
			id3.write(tag.getBytes("ISO-8859-1"));
			id3.writeInt(uData.length + uDesc.length + 12);
			id3.writeShort(0);
			id3.write(1);		// encoding
			id3.write("eng".getBytes("ISO-8859-1"));
			id3.write(0xFF);	// endian1
			id3.write(0xFE);	// endian2
			id3.write(uDesc);	// Unicode string
			id3.writeShort(0);	// Unicode null-string
			id3.write(0xFF);	// endian1
			id3.write(0xFE);	// endian2
			id3.write(uData);	// Unicode string
			id3.writeShort(0);	// Unicode null-string
		} catch (UnsupportedEncodingException e) {
		} catch (IOException e) {
		}
	}
	private void addID3Text(String tag, String data, DataOutputStream id3)
	{
		try {
			byte[] uData = data.getBytes("UTF-16LE");
			id3.write(tag.getBytes("ISO-8859-1"));
			id3.writeInt(uData.length + 5);
			id3.writeShort(0);
			id3.write(1);		// encoding
			id3.write(0xFF);	// endian1
			id3.write(0xFE);	// endian2
			id3.write(uData);	// Unicode string
			id3.writeShort(0);	// null-string
		} catch (UnsupportedEncodingException e) {
		} catch (IOException e) {
		}
	}
	
	// 音声部分を抜き出してストリームに書き込む
	private boolean extractMP3impl(DataOutputStream out) throws IOException
	{
		// signiture
		if (raf.readInt() != 0x464C5601)
			return false;
		// offset to 1st chunk
		raf.readByte();
		int dataOffset = raf.readInt();
		seek(dataOffset);

		// ID3タグを設定する
		byte[] id3sig = new byte[3];
		id3sig[0] = 0x49; id3sig[1] = 0x44; id3sig[2] = 0x33;
		ByteArrayOutputStream id3buf = new ByteArrayOutputStream();
		DataOutputStream id3 = new DataOutputStream(id3buf);
		addID3Comment("", cache.getId(), id3);
		addID3Text("TSSE", "NicoCache_nl", id3);
		addID3Text("TIT2", cache.getTitle(), id3);
		addID3URL("", "http://www.nicovideo.jp/watch/" + cache.getId(), id3);
		
		out.write(id3sig);	// "ID3"
		out.write(3);		// v2.3
		out.write(0);
		out.write(0);		// flag
		
		byte[] id3data = id3buf.toByteArray();
		int length = id3data.length;
		// エスケープ
		length = ((length & ~0x3FFF) << 2) | ((length & ~0x7F & 0x3FFF) << 1) | length & 0x7F;
		out.writeInt(length);
		out.write(id3data);
		
		// reads each chunk
		readUInt32();
		while (fileOffset < fileLength) {
			readChunk(out);
			if (writeFailed)
				break;
			readUInt32();
		}
		return true;
	}
	
	private void readChunk(DataOutputStream out) throws IOException
	{
		long type = readUInt8();
		long dataSize = readUInt24();
		@SuppressWarnings("unused")
		long timeStamp = readUInt24();
		timeStamp |= readUInt8() << 24; 
		readUInt24();
		
		if (dataSize == 0)
			return;
		long media = readUInt8();
		dataSize -= 1;
		fileOffset += dataSize;
		
		// 音声ストリームで、mp3な時だけ
		if (type == 0x8 && (media >> 4) == 2)
		{
			byte[] data = new byte[(int)dataSize];
			raf.read(data, 0, (int)dataSize);
			try {
				out.write(data);
			} catch (IOException e) {
				writeFailed = true;
			}
		}
		else
			seek(fileOffset);
	}
	
	private void seek(int offset) throws IOException {
		raf.seek(offset);
		fileOffset = offset;
	}
	
	private int readUInt8() throws IOException {
		byte b = raf.readByte();
		fileOffset += 1;
		return b & 0xff;
	}
	
	private long readUInt24() throws IOException {
		int[] x = new int[3];
		x[0] = readUInt8();
		x[1] = readUInt8();
		x[2] = readUInt8();
		return (x[0] << 16) + (x[1] << 8) + (x[2] << 0);
	}
	
	private long readUInt32() throws IOException {
		fileOffset += 4;
		return (0xFFFFFFFF & raf.readInt());
	}
}