package jp.sourceforge.nicoro.swf;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.TreeMap;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;

import android.graphics.Bitmap;
import android.graphics.Color;
import android.util.SparseArray;

import jp.gr.java_conf.shiseissi.commonlib.FileUtil;
import jp.sourceforge.nicoro.BinUtil;
import jp.sourceforge.nicoro.FailAnalyzeSwfException;
import jp.sourceforge.nicoro.Log;
import jp.sourceforge.nicoro.Rational;

import static jp.sourceforge.nicoro.swf.Log.DEBUG_LOGV;
import static jp.sourceforge.nicoro.swf.Log.DEBUG_LOGD;
import static jp.sourceforge.nicoro.swf.Log.LOG_TAG;

public class SwfPlayer {

	public SWFFileHeader mHeaderSwf = null;
	public SWFFileHeaderMovie mHeaderMovieSwf = null;
	private SparseArray<BitmapLoader> mBitmapDictionary = new SparseArray<BitmapLoader>();
	private SparseArray<DefineShapeBase> mShapeDictionary = new SparseArray<DefineShapeBase>();
	private TreeMap<Short, PlaceObjectBase> mDisplayList = new TreeMap<Short, PlaceObjectBase>();
	public int mBackgroundColor = Color.BLACK;
	public SoundStreamHead mSoundStreamHead = null;

	private int mFrameCount;

	private SoftReference<ByteBuffer> mExecTagBlockCache = new SoftReference<ByteBuffer>(null);

	private PlayerParent mPlayerParent;
	private SwfPlayer.SoundStreamListener mSoundStreamListener;
	public SwfPlayer() {
	}

	public void create(PlayerParent pp) {
		mPlayerParent = pp;
		mSoundStreamListener = pp.getSoundStreamListener();
	}

	public void preparePlay() {
		mFrameCount = 0;
	}

	public void runMainLoop(RandomAccessFile inSwf)
	throws IOException, FailAnalyzeSwfException {
		RECORDHEADER tagBlock = new RECORDHEADER();
		while (!mPlayerParent.isFinish()) {
			if (mPlayerParent.isSeeking()) {
				seekBySecond(mPlayerParent.getSeekTimeBySecond(), inSwf);
				mPlayerParent.notifySeekCompleted();
				continue;
			}
			if (mPlayerParent.isPause()) {
				try {
					Thread.sleep(10L);
				} catch (InterruptedException e) {
				}
				continue;
			}
			tagBlock.read(inSwf);
			if (isEndTag(tagBlock)) {
				break;
			}

			execTagBlock(tagBlock, inSwf, false);
		}
	}

    private static boolean isEndTag(RECORDHEADER tagBlock) {
    	boolean retval = (tagBlock.tagType == RECORDHEADER.SWFTAG_END);
    	assert retval ? (tagBlock.tagLength == 0) : true;
    	return retval;
    }

	/**
	 *
	 * @param inSwf
	 * @return FWSならinSwfそのまま、CWSならInflaterInputStreamでラッピングしたもの
	 * @throws IOException
	 * @throws FailAnalyzeSwfException
	 */
    public InputStream readSWFFileHeaderWhole(InputStream inSwf)
    throws IOException, FailAnalyzeSwfException {
		mHeaderSwf = new SWFFileHeader();
		SwfPlayer.readSWFFileHeader(inSwf, mHeaderSwf);

		if (Arrays.equals(mHeaderSwf.signature, SWFFileHeader.CWS)) {
//			inSwf = new GZIPInputStream(inSwf);
			inSwf = new InflaterInputStream(inSwf, new Inflater(false));
		}

		mHeaderMovieSwf = new SWFFileHeaderMovie();
		SwfPlayer.readSWFFileHeaderMovie(inSwf, mHeaderMovieSwf);
		return inSwf;
    }

    private static void readSWFFileHeader(InputStream inSwf, SWFFileHeader header)
    throws IOException, FailAnalyzeSwfException {
    	int readSize = FileUtil.readComplete(inSwf, header.signature);
    	if (readSize != header.signature.length) {
    		throw new FailAnalyzeSwfException("header magic read failed: " + readSize);
    	}
    	if (DEBUG_LOGD) {
    		char[] logMagic = new char[3];
    		for (int i = 0; i < logMagic.length; ++i) {
    			logMagic[i] = (char) header.signature[i];
    		}
    		DecimalFormat df = new DecimalFormat();
            df.applyPattern("0");
    		Log.d(LOG_TAG, Log.buf().append("SWF header magic=").append(new String(logMagic))
    				.append(String.format(" {%02X,%02X,%02X}", header.signature[0], header.signature[1], header.signature[2])).toString());
    	}
    	if (!Arrays.equals(header.signature, SWFFileHeader.FWS)
    			&& !Arrays.equals(header.signature, SWFFileHeader.CWS)) {
    		throw new FailAnalyzeSwfException("header magic is invalid.");
    	}

    	int ver = inSwf.read();
    	if (ver < 0) {
    		throw new FailAnalyzeSwfException("header ver read failed.");
    	}
    	header.version = (byte) ver;
    	if (DEBUG_LOGD) {
    		Log.d(LOG_TAG, Log.buf().append("SWF header ver=").append(header.version).toString());
    	}

    	ByteBuffer fileLengthBuffer = ByteBuffer.allocate(4);
    	fileLengthBuffer.order(ByteOrder.LITTLE_ENDIAN);
    	readSize = FileUtil.readComplete(inSwf, fileLengthBuffer.array());
    	if (readSize != fileLengthBuffer.array().length) {
    		throw new FailAnalyzeSwfException("header file_length read failed: " + readSize);
    	}
    	header.fileLength = fileLengthBuffer.getInt();
    	if (DEBUG_LOGD) {
    		Log.d(LOG_TAG, Log.buf().append("SWF header file_length=").append(header.fileLength).toString());
    	}
    }

    private static void readSWFFileHeaderMovie(InputStream inSwf, SWFFileHeaderMovie headerMovie)
    throws IOException, FailAnalyzeSwfException {
    	headerMovie.frameSize.read(inSwf);

    	int readByte = inSwf.read();
    	if (readByte < 0) {
    		throw new FailAnalyzeSwfException("header_movie FrameRate:decimal read failed.");
    	}
    	headerMovie.frameRateDecimal = readByte;
    	if (DEBUG_LOGD) {
    		Log.d(LOG_TAG, Log.buf().append("SWF header_movie FrameRate:decimal=")
    				.append(headerMovie.frameRateDecimal).toString());
    	}

    	readByte = inSwf.read();
    	if (readByte < 0) {
    		throw new FailAnalyzeSwfException("header_movie FrameRate:integral read failed.");
    	}
    	headerMovie.frameRateIntegral = readByte;
    	if (DEBUG_LOGD) {
    		Log.d(LOG_TAG, Log.buf().append("SWF header_movie FrameRate:integral=")
    				.append(headerMovie.frameRateIntegral).toString());
    	}

    	ByteBuffer frameRateCountBuffer = ByteBuffer.allocate(2);
    	frameRateCountBuffer.order(ByteOrder.LITTLE_ENDIAN);
    	int readSize = FileUtil.readComplete(inSwf, frameRateCountBuffer.array());
    	if (readSize != frameRateCountBuffer.array().length) {
    		throw new FailAnalyzeSwfException("header_movie FrameCount read failed: " + readSize);
    	}
    	headerMovie.frameCount = (int) frameRateCountBuffer.getShort() & 0xffff;
    	if (DEBUG_LOGD) {
    		Log.d(LOG_TAG, Log.buf().append("SWF header_movie FrameCount=")
    				.append(headerMovie.frameCount).toString());
    	}
    }

	/**
	 *
	 * @param inSwf
	 * @throws IOException
	 * @throws FailAnalyzeSwfException
	 */
    public void readSWFFileHeaderWhole(RandomAccessFile inSwf)
    throws IOException, FailAnalyzeSwfException {
		mHeaderSwf = new SWFFileHeader();
		SwfPlayer.readSWFFileHeader(inSwf, mHeaderSwf);

		if (Arrays.equals(mHeaderSwf.signature, SWFFileHeader.CWS)) {
			assert false : "CWS must be converted to FWS";
			throw new FailAnalyzeSwfException("CWS must be converted to FWS");
		}

		mHeaderMovieSwf = new SWFFileHeaderMovie();
		SwfPlayer.readSWFFileHeaderMovie(inSwf, mHeaderMovieSwf);
    }

    private static void readSWFFileHeader(RandomAccessFile inSwf, SWFFileHeader header)
    throws IOException, FailAnalyzeSwfException {
    	int readSize = FileUtil.readComplete(inSwf, header.signature);
    	if (readSize != header.signature.length) {
    		throw new FailAnalyzeSwfException("header magic read failed: " + readSize);
    	}
    	if (DEBUG_LOGD) {
    		char[] logMagic = new char[3];
    		for (int i = 0; i < logMagic.length; ++i) {
    			logMagic[i] = (char) header.signature[i];
    		}
    		DecimalFormat df = new DecimalFormat();
            df.applyPattern("0");
    		Log.d(LOG_TAG, Log.buf().append("SWF header magic=").append(new String(logMagic))
    				.append(String.format(" {%02X,%02X,%02X}", header.signature[0], header.signature[1], header.signature[2])).toString());
    	}
    	if (!Arrays.equals(header.signature, SWFFileHeader.FWS)
    			&& !Arrays.equals(header.signature, SWFFileHeader.CWS)) {
    		throw new FailAnalyzeSwfException("header magic is invalid.");
    	}

    	int ver = inSwf.read();
    	if (ver < 0) {
    		throw new FailAnalyzeSwfException("header ver read failed.");
    	}
    	header.version = (byte) ver;
    	if (DEBUG_LOGD) {
    		Log.d(LOG_TAG, Log.buf().append("SWF header ver=").append(header.version).toString());
    	}

    	ByteBuffer fileLengthBuffer = ByteBuffer.allocate(4);
    	fileLengthBuffer.order(ByteOrder.LITTLE_ENDIAN);
    	readSize = FileUtil.readComplete(inSwf, fileLengthBuffer.array());
    	if (readSize != fileLengthBuffer.array().length) {
    		throw new FailAnalyzeSwfException("header file_length read failed: " + readSize);
    	}
    	header.fileLength = fileLengthBuffer.getInt();
    	if (DEBUG_LOGD) {
    		Log.d(LOG_TAG, Log.buf().append("SWF header file_length=").append(header.fileLength).toString());
    	}
    }

    private static void readSWFFileHeaderMovie(RandomAccessFile inSwf, SWFFileHeaderMovie headerMovie)
    throws IOException, FailAnalyzeSwfException {
    	headerMovie.frameSize.read(inSwf);

    	int readByte = inSwf.read();
    	if (readByte < 0) {
    		throw new FailAnalyzeSwfException("header_movie FrameRate:decimal read failed.");
    	}
    	headerMovie.frameRateDecimal = readByte;
    	if (DEBUG_LOGD) {
    		Log.d(LOG_TAG, Log.buf().append("SWF header_movie FrameRate:decimal=")
    				.append(headerMovie.frameRateDecimal).toString());
    	}

    	readByte = inSwf.read();
    	if (readByte < 0) {
    		throw new FailAnalyzeSwfException("header_movie FrameRate:integral read failed.");
    	}
    	headerMovie.frameRateIntegral = readByte;
    	if (DEBUG_LOGD) {
    		Log.d(LOG_TAG, Log.buf().append("SWF header_movie FrameRate:integral=")
    				.append(headerMovie.frameRateIntegral).toString());
    	}

    	ByteBuffer frameRateCountBuffer = ByteBuffer.allocate(2);
    	frameRateCountBuffer.order(ByteOrder.LITTLE_ENDIAN);
    	int readSize = FileUtil.readComplete(inSwf, frameRateCountBuffer.array());
    	if (readSize != frameRateCountBuffer.array().length) {
    		throw new FailAnalyzeSwfException("header_movie FrameCount read failed: " + readSize);
    	}
    	headerMovie.frameCount = (int) frameRateCountBuffer.getShort() & 0xffff;
    	if (DEBUG_LOGD) {
    		Log.d(LOG_TAG, Log.buf().append("SWF header_movie FrameCount=")
    				.append(headerMovie.frameCount).toString());
    	}
    }

    public static int readSHAPERECORDs(ArrayList<SHAPERECORD> shapeRecords,
    		byte numFillBits, byte numLineBits,
    		byte[] bufferArray, int offset, int bufferLength, int tagType) throws FailAnalyzeSwfException {
    	int lengthRemain = bufferLength - offset;
    	byte[] binary = BinUtil.toBinaryFromBytes(bufferArray, offset * 8, lengthRemain * 8);

    	int offsetBits = 0;
    	while (true) {
    		boolean typeFlag = (binary[offsetBits] != 0);
	    	if (DEBUG_LOGD) {
	    		Log.d(LOG_TAG, Log.buf().append("TypeFlag=").append(typeFlag).toString());
	    	}
    		++offsetBits;

    		if (typeFlag) {
    			// STRAIGHTEDGERECORD or CURVEDEDGERECORD
    			boolean straightFlag = (binary[offsetBits] != 0);
    	    	if (DEBUG_LOGD) {
    	    		Log.d(LOG_TAG, Log.buf().append("StraightFlag=").append(straightFlag).toString());
    	    	}
    	    	++offsetBits;

    	    	if (straightFlag) {
    		    	if (DEBUG_LOGD) {
    		    		Log.d(LOG_TAG, Log.buf().append("STRAIGHTEDGERECORD: offsetBits=")
    		    				.append(offsetBits).toString());
    		    	}
    	    		STRAIGHTEDGERECORD straightEdge = new STRAIGHTEDGERECORD();
    	    		straightEdge.typeFlag = typeFlag;
    	    		straightEdge.straightFlag = straightFlag;

    	    		straightEdge.numBits = BinUtil.toByteFromBinary(binary, offsetBits, 4);
        	    	if (DEBUG_LOGD) {
        	    		Log.d(LOG_TAG, Log.buf().append("NumBits=").append(straightEdge.numBits).toString());
        	    	}
    	    		offsetBits += 4;

    	    		straightEdge.generalLineFlag = (binary[offsetBits] != 0);
        	    	if (DEBUG_LOGD) {
        	    		Log.d(LOG_TAG, Log.buf().append("GeneralLineFlag=")
        	    				.append(straightEdge.generalLineFlag).toString());
        	    	}
        	    	++offsetBits;

        	    	if (!straightEdge.generalLineFlag) {
        	    		straightEdge.vertLineFlag = (binary[offsetBits] != 0);
            	    	if (DEBUG_LOGD) {
            	    		Log.d(LOG_TAG, Log.buf().append("VertLineFlag=")
            	    				.append(straightEdge.vertLineFlag).toString());
            	    	}
            	    	++offsetBits;
        	    	}

        	    	if (straightEdge.generalLineFlag || !straightEdge.vertLineFlag) {
        	    		straightEdge.deltaX = BinUtil.toSLongFromBinary(binary, offsetBits, straightEdge.numBits + 2);
            	    	if (DEBUG_LOGD) {
            	    		Log.d(LOG_TAG, Log.buf().append("DeltaX=").append(straightEdge.deltaX).toString());
            	    	}
            	    	offsetBits += straightEdge.numBits + 2;
        	    	} else if (straightEdge.generalLineFlag || straightEdge.vertLineFlag) {
        	    		straightEdge.deltaY = BinUtil.toSLongFromBinary(binary, offsetBits, straightEdge.numBits + 2);
            	    	if (DEBUG_LOGD) {
            	    		Log.d(LOG_TAG, Log.buf().append("DeltaY=").append(straightEdge.deltaY).toString());
            	    	}
            	    	offsetBits += straightEdge.numBits + 2;
        	    	}

    				shapeRecords.add(straightEdge);
    	    	} else {
    		    	if (DEBUG_LOGD) {
    		    		Log.d(LOG_TAG, Log.buf().append("CURVEDEDGERECORD: offsetBits=").append(offsetBits).toString());
    		    	}
    	    		CURVEDEDGERECORD curvedEdge = new CURVEDEDGERECORD();
    	    		curvedEdge.typeFlag = typeFlag;
    	    		curvedEdge.straightFlag = straightFlag;

    	    		curvedEdge.numBits = BinUtil.toByteFromBinary(binary, offsetBits, 4);
        	    	if (DEBUG_LOGD) {
        	    		Log.d(LOG_TAG, Log.buf().append("NumBits=").append(curvedEdge.numBits).toString());
        	    	}
    	    		offsetBits += 4;

    	    		curvedEdge.controlDeltaX = BinUtil.toSLongFromBinary(binary, offsetBits, curvedEdge.numBits + 2);
        	    	if (DEBUG_LOGD) {
        	    		Log.d(LOG_TAG, Log.buf().append("ControlDeltaX=").append(curvedEdge.controlDeltaX).toString());
        	    	}
        	    	offsetBits += curvedEdge.numBits + 2;

        	    	// TODO ArrayIndexOutOfBoundsException: stateNewStylesのバグの遠因かもしれないので様子見 6/4
    	    		curvedEdge.controlDeltaY = BinUtil.toSLongFromBinary(binary, offsetBits, curvedEdge.numBits + 2);
        	    	if (DEBUG_LOGD) {
        	    		Log.d(LOG_TAG, Log.buf().append("ControlDeltaY=").append(curvedEdge.controlDeltaY).toString());
        	    	}
        	    	offsetBits += curvedEdge.numBits + 2;

    	    		curvedEdge.anchorDeltaX = BinUtil.toSLongFromBinary(binary, offsetBits, curvedEdge.numBits + 2);
        	    	if (DEBUG_LOGD) {
        	    		Log.d(LOG_TAG, Log.buf().append("AnchorDeltaX=").append(curvedEdge.anchorDeltaX).toString());
        	    	}
        	    	offsetBits += curvedEdge.numBits + 2;

                    // TODO ArrayIndexOutOfBoundsException: stateNewStylesのバグの遠因かもしれないので様子見 6/4
    	    		curvedEdge.anchorDeltaY = BinUtil.toSLongFromBinary(binary, offsetBits, curvedEdge.numBits + 2);
        	    	if (DEBUG_LOGD) {
        	    		Log.d(LOG_TAG, Log.buf().append("AnchorDeltaY=").append(curvedEdge.anchorDeltaY).toString());
        	    	}
        	    	offsetBits += curvedEdge.numBits + 2;

    				shapeRecords.add(curvedEdge);
    	    	}
    		} else {
    			// ENDSHAPERECORD or STYLECHANGERECORD
    			byte endFlag = BinUtil.toByteFromBinary(binary, offsetBits, 5);
    			if (endFlag == 0) {
    		    	if (DEBUG_LOGD) {
    		    		Log.d(LOG_TAG, Log.buf().append("ENDSHAPERECORD: offsetBits=").append(offsetBits).toString());
    		    	}
    				ENDSHAPERECORD end = new ENDSHAPERECORD();
    				end.typeFlag = typeFlag;
    				shapeRecords.add(end);
    				offsetBits += 5;
    				break;
    			} else {
    		    	if (DEBUG_LOGD) {
    		    		Log.d(LOG_TAG, Log.buf().append("STYLECHANGERECORD: offsetBits=").append(offsetBits).toString());
    		    	}
    				STYLECHANGERECORD styleChange = new STYLECHANGERECORD();
    				styleChange.typeFlag = typeFlag;
    				if (tagType == RECORDHEADER.SWFTAG_DEFINESHAPE2
    				        || tagType == RECORDHEADER.SWFTAG_DEFINESHAPE3) {
                        styleChange.stateNewStyles = (binary[offsetBits] != 0);
                        ++offsetBits;
    				} else {
    				    styleChange.stateNewStyles = false;
                        ++offsetBits; // ENDSHAPERECORDとの兼ね合いも考えると、どうもいるっぽい
    				}
    				styleChange.stateLineStyle = (binary[offsetBits + 0] != 0);
    				styleChange.stateFillStyle1 = (binary[offsetBits + 1] != 0);
    				styleChange.stateFillStyle0 = (binary[offsetBits + 2] != 0);
    				styleChange.stateMoveTo = (binary[offsetBits + 3] != 0);
    		    	if (DEBUG_LOGD) {
    		    		Log.d(LOG_TAG, Log.buf()
    		    				.append("StateNewStyles=").append(styleChange.stateNewStyles)
    		    				.append(" StateLineStyle=").append(styleChange.stateLineStyle)
    		    				.append(" StateFillStyle1=").append(styleChange.stateFillStyle1)
    		    				.append(" StateFillStyle0=").append(styleChange.stateFillStyle0)
    		    				.append(" StateMoveTo=").append(styleChange.stateMoveTo).toString());
    		    	}
    				offsetBits += 4;

    		    	if (styleChange.stateMoveTo) {
    		    		styleChange.moveBits = BinUtil.toByteFromBinary(binary, offsetBits, 5);
        		    	if (DEBUG_LOGD) {
        		    		Log.d(LOG_TAG, Log.buf().append("MoveBits=").append(styleChange.moveBits).toString());
        		    	}
        		    	offsetBits += 5;
        		    	styleChange.moveDeltaX = BinUtil.toIntFromBinary(binary, offsetBits, styleChange.moveBits);
        		    	if (DEBUG_LOGD) {
        		    		Log.d(LOG_TAG, Log.buf().append("MoveDeltaX=").append(styleChange.moveDeltaX).toString());
        		    	}
        		    	offsetBits += styleChange.moveBits;
        		    	styleChange.moveDeltaX = BinUtil.toIntFromBinary(binary, offsetBits, styleChange.moveBits);
        		    	if (DEBUG_LOGD) {
        		    		Log.d(LOG_TAG, Log.buf().append("MoveDeltaY=").append(styleChange.moveDeltaY).toString());
        		    	}
        		    	offsetBits += styleChange.moveBits;
    		    	}

    		    	if (styleChange.stateFillStyle0) {
        		    	styleChange.fillStyle0 = BinUtil.toShortFromBinary(binary, offsetBits, numFillBits);
        		    	if (DEBUG_LOGD) {
        		    		Log.d(LOG_TAG, Log.buf().append("FillStyle0=").append(styleChange.fillStyle0).toString());
        		    	}
        		    	offsetBits += numFillBits;
    		    	}
    		    	if (styleChange.stateFillStyle1) {
        		    	styleChange.fillStyle1 = BinUtil.toShortFromBinary(binary, offsetBits, numFillBits);
        		    	if (DEBUG_LOGD) {
        		    		Log.d(LOG_TAG, Log.buf().append("FillStyle1=").append(styleChange.fillStyle1).toString());
        		    	}
        		    	offsetBits += numFillBits;
    		    	}

    		    	if (styleChange.stateLineStyle) {
        		    	styleChange.lineStyle = BinUtil.toShortFromBinary(binary, offsetBits, numLineBits);
        		    	if (DEBUG_LOGD) {
        		    		Log.d(LOG_TAG, Log.buf().append("LineStyle=").append(styleChange.lineStyle).toString());
        		    	}
        		    	offsetBits += numLineBits;
    		    	}

    		    	if (styleChange.stateNewStyles) {
    		    		styleChange.fillStyles = new FILLSTYLEARRAY();
    		    		int offsetNext = offset + (offsetBits + 7) / 8;
    		    		offsetNext = styleChange.fillStyles.read(bufferArray, offsetNext, tagType);
    		    		styleChange.lineStyles = new LINESTYLEARRAY();
    		    		offsetNext = styleChange.lineStyles.read(bufferArray, offsetNext, tagType);
    		    		styleChange.numFillBits = (byte) ((bufferArray[offsetNext] >> 4) & 0x0f);
    		    		styleChange.numLineBits = (byte) (bufferArray[offsetNext] & 0x0f);
        		    	if (DEBUG_LOGD) {
        		    		Log.d(LOG_TAG, Log.buf()
        		    				.append("NumFillBits=").append(styleChange.numFillBits)
        		    				.append(" NumLineBits=").append(styleChange.numLineBits).toString());
        		    	}
        		    	offsetNext += 1;

        		    	offsetBits = (offsetNext - offset) * 8;
    		    	}

    				shapeRecords.add(styleChange);
    			}
    		}
    	}

    	return offset + (offsetBits + 7) / 8;
    }

    public Bitmap getBitmapFromDictionary(short id) throws IOException, FailAnalyzeSwfException {
    	return mBitmapDictionary.get(id).get();
    }
    public void putBitmapToDictionary(short id, BitmapLoader bitmapLoader) {
		mBitmapDictionary.put(id, bitmapLoader);
    }

    public void putPlaceObjectToDisplayList(short depth, PlaceObjectBase placeObject) {
		mDisplayList.put(depth, placeObject);
    }
    public void removePlaceObjectFromDisplayList(short depth) {
    	PlaceObjectBase removed = mDisplayList.remove(depth);
    	assert removed != null;
    }
    public Collection<PlaceObjectBase> getDisplayListValues() {
    	return mDisplayList.values();
    }

    public DefineShapeBase getShapeFromDictionary(short id) {
    	return mShapeDictionary.get(id);
    }
    public void putShapeToDictionary(short id, DefineShapeBase defineShape) {
    	mShapeDictionary.put(id, defineShape);
    }

    public void execTagBlock(RECORDHEADER tagBlock, RandomAccessFile inSwf,
    		boolean frameSkip)
    throws IOException, FailAnalyzeSwfException {
    	int length = tagBlock.tagLengthLong >= 0 ? tagBlock.tagLengthLong : tagBlock.tagLength;
    	ByteBuffer buffer;
    	byte[] bufferArray;
    	int readSize;

    	SwfPlayer swfPlayer = this;
    	SwfPlayer.SoundStreamListener soundStreamListener = mSoundStreamListener;

    	switch (tagBlock.tagType) {
    	case RECORDHEADER.SWFTAG_END:
    		assert false : "logic error: SWFTAG_END must be preprocessed.";
    		break;
    	case RECORDHEADER.SWFTAG_SHOWFRAME:
    		assert length == 0;
    		if (length > 0) {
	    		long skipSize = inSwf.skipBytes(length);
	        	if (skipSize != length) {
	        		throw new FailAnalyzeSwfException("tag block contents skip failed: " + skipSize);
	        	}
    		}
			showFrame(frameSkip);
    		break;
    	case RECORDHEADER.SWFTAG_DEFINESHAPE: {
            buffer = getExecTagBlockCache(length);
            bufferArray = buffer.array();
            readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
            if (readSize != length) {
                throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
            }

            DefineShape defineShape = new DefineShape();
            defineShape.shapeId = buffer.getShort();
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, Log.buf().append("ShapeId=")
                        .append(defineShape.shapeId).toString());
            }

            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "ShapeBounds");
            }
            int offsetShapes = defineShape.shapeBounds.read(bufferArray, 2);
            defineShape.shapes.read(bufferArray, offsetShapes, length, RECORDHEADER.SWFTAG_DEFINESHAPE);
            swfPlayer.putShapeToDictionary(defineShape.shapeId, defineShape);
    	} break;
    	case RECORDHEADER.SWFTAG_SETBACKGROUNDCOLOR:
    		buffer = getExecTagBlockCache(length);
    		bufferArray = buffer.array();
    		readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
    		if (readSize != length) {
        		throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
    		}
    		if (length < 3) {
        		throw new FailAnalyzeSwfException("tag block contents SetBackgroundColor doesn't have RGB.");
    		}
    		swfPlayer.mBackgroundColor = Color.rgb(bufferArray[0], bufferArray[1], bufferArray[2]);
    		if (DEBUG_LOGD) {
    			Log.d(LOG_TAG, Log.buf().append("BackgroundColor=")
    					.append(Integer.toHexString(swfPlayer.mBackgroundColor)).toString());
    		}
    		break;
    	case RECORDHEADER.SWFTAG_DEFINEBITSJPEG2:
    		{
//	    		PartialInputStreamRandomAccessWrapper partialIn =
//	    			new PartialInputStreamRandomAccessWrapper(inSwf, length);
	        	DefineBitsJPEG2 defineBitsJPEG2 = new DefineBitsJPEG2();
	        	defineBitsJPEG2.read(inSwf, length, swfPlayer);
    		}
    		break;
    	case RECORDHEADER.SWFTAG_DEFINESHAPE2: {
            buffer = getExecTagBlockCache(length);
            bufferArray = buffer.array();
            readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
            if (readSize != length) {
                throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
            }

            DefineShape2 defineShape2 = new DefineShape2();
            defineShape2.shapeId = buffer.getShort();
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, Log.buf().append("ShapeId=")
                        .append(defineShape2.shapeId).toString());
            }

            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "ShapeBounds");
            }
            int offsetShapes = defineShape2.shapeBounds.read(bufferArray, 2);
            defineShape2.shapes.read(bufferArray, offsetShapes, length, RECORDHEADER.SWFTAG_DEFINESHAPE2);
            swfPlayer.putShapeToDictionary(defineShape2.shapeId, defineShape2);
    	} break;
    	case RECORDHEADER.SWFTAG_FILEATTRIBUTES:
    		buffer = getExecTagBlockCache(length);
    		bufferArray = buffer.array();
    		readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
    		if (readSize != length) {
        		throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
    		}
    		if (DEBUG_LOGD) {
    			Log.d(LOG_TAG, Log.buf()
    					.append("UseDirectBlit=").append((bufferArray[0] & 0x40) != 0)
    					.append(" UseGPU=").append((bufferArray[0] & 0x20) != 0)
    					.append(" HasMetadata=").append((bufferArray[0] & 0x10) != 0)
    					.append(" ActionScript3=").append((bufferArray[0] & 0x08) != 0)
    					.append(" UseNetwork=").append((bufferArray[0] & 0x01) != 0).toString());
    		}
    		break;
    	case RECORDHEADER.SWFTAG_PLACEOBJECT2:
    		{
	    		buffer = getExecTagBlockCache(length);
	    		bufferArray = buffer.array();
	    		readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
	    		if (readSize != length) {
	        		throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
	    		}

	    		PlaceObject2 placeObject2 = new PlaceObject2();
	    		placeObject2.read(buffer);

	    		swfPlayer.putPlaceObjectToDisplayList(placeObject2.depth, placeObject2);
    		}
    		break;
    	case RECORDHEADER.SWFTAG_REMOVEOBJECT2:
    		buffer = getExecTagBlockCache(length);
    		bufferArray = buffer.array();
    		readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
    		if (readSize != length) {
        		throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
    		}
    		short depth = buffer.getShort();	// UI16
    		if (DEBUG_LOGD) {
    			Log.d(LOG_TAG, Log.buf().append("Depth=").append(depth).toString());
    		}
    		swfPlayer.removePlaceObjectFromDisplayList(depth);
    		break;
    	case RECORDHEADER.SWFTAG_DEFINESHAPE3: {
    		buffer = getExecTagBlockCache(length);
    		bufferArray = buffer.array();
    		readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
    		if (readSize != length) {
        		throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
    		}

    		DefineShape3 defineShape3 = new DefineShape3();
    		defineShape3.shapeId = buffer.getShort();
    		if (DEBUG_LOGD) {
    			Log.d(LOG_TAG, Log.buf().append("ShapeId=")
    					.append(defineShape3.shapeId).toString());
    		}

    		if (DEBUG_LOGD) {
    			Log.d(LOG_TAG, "ShapeBounds");
    		}
    		int offsetShapes = defineShape3.shapeBounds.read(bufferArray, 2);
    		defineShape3.shapes.read(bufferArray, offsetShapes, length, RECORDHEADER.SWFTAG_DEFINESHAPE3);
    		swfPlayer.putShapeToDictionary(defineShape3.shapeId, defineShape3);
    	} break;
    	case RECORDHEADER.SWFTAG_SOUNDSTREAMHEAD:
    		buffer = getExecTagBlockCache(length);
    		bufferArray = buffer.array();
    		readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
    		if (readSize != length) {
        		throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
    		}

    		SoundStreamHead soundStreamHead = new SoundStreamHead();
    		if (swfPlayer.mSoundStreamHead != null) {
    			if (DEBUG_LOGD) {
    				Log.d(LOG_TAG, "Plural SoundStreamHead are found");
    			}
    		}
    		swfPlayer.mSoundStreamHead = soundStreamHead;

    		soundStreamHead.readSoundStreamHead(bufferArray, 0);

    		soundStreamListener.onSoundStreamHead(soundStreamHead, swfPlayer);

    		break;
    	case RECORDHEADER.SWFTAG_SOUNDSTREAMBLOCK:
    		buffer = getExecTagBlockCache(length);
    		bufferArray = buffer.array();
    		readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
    		if (readSize != length) {
        		throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
    		}
    		if (DEBUG_LOGD) {
    			Log.d(LOG_TAG, Log.buf().append("SoundStreamBlock: length=")
    					.append(length).toString());
    		}

//    		execSoundStreamBlock(buffer);
    		StreamSoundDataBase streamSoundData =
    			swfPlayer.mSoundStreamHead.createStreamSoundData();
    		streamSoundData.read(bufferArray, 0, length);

    		soundStreamListener.onSoundStreamBlock(streamSoundData,
    				swfPlayer.mSoundStreamHead, swfPlayer, frameSkip);

    		break;
    	case RECORDHEADER.SWFTAG_DEFINEBITSLOSSLESS2:
	    	{
	    		// TODO BitmapのObject開放しない動画があるから回避策が必要

//	    		PartialInputStreamRandomAccessWrapper partialIn =
//	    			new PartialInputStreamRandomAccessWrapper(inSwf, length);
	        	DefineBitsLossless2 defineBitsLossless2 = new DefineBitsLossless2();
	        	defineBitsLossless2.read(inSwf, length, swfPlayer);
	    	}
	    	break;
    	case RECORDHEADER.SWFTAG_DEFINESPRITE:
            buffer = getExecTagBlockCache(length);
            bufferArray = buffer.array();
            readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
            if (readSize != length) {
                throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
            }
            DefineSprite defineSprite = new DefineSprite();
            defineSprite.read(bufferArray, 0);
    	    // TODO ControlTagsの取り扱い
    	    break;
    	default:
    		throw new FailAnalyzeSwfException("Not supported SWF tag=" + tagBlock.tagType);
//    		Log.w(LOG_TAG, "Not supported SWF tag=" + tagBlock.tagType);
//    		long skipSize = inSwf.skip(length);
//        	if (skipSize != length) {
//        		throw new FailAnalyzeSwfException("tag block contents skip failed: " + skipSize);
//        	}
//    		break;
    	}
    }

    private ByteBuffer getExecTagBlockCache(int length) {
        ByteBuffer buffer = mExecTagBlockCache.get();
        if (buffer == null || buffer.capacity() < length) {
            buffer = ByteBuffer.allocate(length);
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            mExecTagBlockCache = new SoftReference<ByteBuffer>(buffer);
        } else {
            buffer.clear();
            buffer.limit(length);
        }
        return buffer;
    }

    public void showFrame(boolean frameSkip) throws FailAnalyzeSwfException, IOException {
    	if (!frameSkip) {
    		mPlayerParent.showFrame();
    	}
        ++mFrameCount;
    }

    public static boolean getFWSFile(File inSwfFile, File outFile) throws IOException, FailAnalyzeSwfException {
    	InputStream inSwf = null;
    	OutputStream out = null;
    	try {
    		inSwf = new FileInputStream(inSwfFile);
    		inSwf = new BufferedInputStream(inSwf);
	    	SWFFileHeader header = new SWFFileHeader();
	    	readSWFFileHeader(inSwf, header);
			if (Arrays.equals(header.signature, SWFFileHeader.FWS)) {
				return false;
			} else if (Arrays.equals(header.signature, SWFFileHeader.CWS)) {
				// FWSに変換
				out = new FileOutputStream(outFile);
				out = new BufferedOutputStream(out);
				writeFWSHeader(out, header);
				inSwf = new InflaterInputStream(inSwf, new Inflater(false));
				byte[] buffer = new byte[1024];
				while (true) {
					int readSize = inSwf.read(buffer);
					if (readSize < 0) {
						break;
					}
					out.write(buffer, 0, readSize);
				}
				return true;
			} else {
				throw new FailAnalyzeSwfException("SWF signature invalid="
				        + Arrays.toString(header.signature));
			}

    	} finally {
    		if (inSwf != null) {
    			try {
    				inSwf.close();
    			} catch (IOException e) {
    				Log.d(LOG_TAG, e.toString(), e);
    			}
    		}
    		if (out != null) {
    			try {
    				out.close();
    			} catch (IOException e) {
    				Log.d(LOG_TAG, e.toString(), e);
    			}
    		}
    	}
    }

    private static void writeFWSHeader(OutputStream out, SWFFileHeader header)
    throws IOException {
    	out.write(SWFFileHeader.FWS);
    	out.write(header.version);
    	ByteBuffer fileLengthBuffer = ByteBuffer.allocate(4);
    	fileLengthBuffer.order(ByteOrder.LITTLE_ENDIAN);
    	fileLengthBuffer.putInt(header.fileLength);
    	out.write(fileLengthBuffer.array());
    }

    public static boolean getAudioFile(InputStream inSwf, File outAudioFile) throws IOException, FailAnalyzeSwfException {
    	SWFFileHeader header = new SWFFileHeader();
    	readSWFFileHeader(inSwf, header);
    	SWFFileHeaderMovie headerMovie = new SWFFileHeaderMovie();
    	readSWFFileHeaderMovie(inSwf, headerMovie);

		ByteBuffer buffer = null;
		byte[] bufferArray;
		int readSize;
		SoundStreamHead soundStreamHead = null;

		RECORDHEADER tagBlock = new RECORDHEADER();
    	OutputStream out = null;
    	boolean written = false;
    	try {
			while (true) {
				tagBlock.read(inSwf);
				if (isEndTag(tagBlock)) {
					break;
				}

		    	int length = tagBlock.tagLengthLong >= 0 ? tagBlock.tagLengthLong : tagBlock.tagLength;
		    	switch (tagBlock.tagType) {
		    	case RECORDHEADER.SWFTAG_END:
		    		assert false : "logic error: SWFTAG_END must be preprocessed.";
		    		break;
		    	case RECORDHEADER.SWFTAG_SOUNDSTREAMHEAD:
		    	    if (buffer == null || buffer.capacity() < length) {
	                    buffer = ByteBuffer.allocate(length);
	                    buffer.order(ByteOrder.LITTLE_ENDIAN);
		    	    } else {
		                buffer.clear();
		                buffer.limit(length);
		    	    }
		    		bufferArray = buffer.array();
		    		readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
		    		if (readSize != length) {
		        		throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
		    		}

		    		soundStreamHead = new SoundStreamHead();
		    		soundStreamHead.readSoundStreamHead(bufferArray, 0);

		    		out = new FileOutputStream(outAudioFile);
		    		out = new BufferedOutputStream(out);

		    		break;
		    	case RECORDHEADER.SWFTAG_SOUNDSTREAMBLOCK:
                    if (buffer == null || buffer.capacity() < length) {
                        buffer = ByteBuffer.allocate(length);
                        buffer.order(ByteOrder.LITTLE_ENDIAN);
                    } else {
                        buffer.clear();
                        buffer.limit(length);
                    }
		    		bufferArray = buffer.array();
		    		readSize = FileUtil.readComplete(inSwf, bufferArray, 0, length);
		    		if (readSize != length) {
		        		throw new FailAnalyzeSwfException("tag block contents read failed: " + readSize);
		    		}
		    		if (soundStreamHead == null) {
		        		throw new FailAnalyzeSwfException("SoundStreamBlock is found before SoundStreamHead.");
		    		}
		    		assert out != null;
		    		StreamSoundDataBase streamSoundData =
		    			soundStreamHead.createStreamSoundData();
		    		streamSoundData.read(bufferArray, 0, length);

		    		streamSoundData.writeData(out);
		    		written = true;
		    		break;
	    		default:
	    		    int skipLength = length;
	    		    while (skipLength > 0) {
	    		        skipLength -= inSwf.skip(skipLength);
	    		    }
	    		    assert skipLength == 0;
	    			break;
		    	}
			}

			if (out == null) {
				return false;
			} else {
				return written;
			}
    	} finally {
    		if (out != null) {
    			try {
    				out.close();
    			} catch (IOException e) {
    				Log.d(LOG_TAG, e.toString(), e);
    			}
    		}
    	}
    }

    public int getFrameCount() {
    	return mFrameCount;
    }

	public void getCurrentPosition(Rational rational) {
		rational.num = mFrameCount << 8;
		rational.den = (mHeaderMovieSwf.frameRateIntegral << 8) | mHeaderMovieSwf.frameRateDecimal;
	}

    private void seekBySecond(int second, RandomAccessFile inSwf)
    throws IOException, FailAnalyzeSwfException {
		// 切り上げ
		int den = (mHeaderMovieSwf.frameRateIntegral << 8) | mHeaderMovieSwf.frameRateDecimal;
		int num = (mFrameCount << 8) + den - 1;
		int currentSecond = num / den;
		if (DEBUG_LOGV) {
			Log.v(LOG_TAG, Log.buf().append("seek current=").append(currentSecond)
					.append(" to=").append(second).toString());
		}

		if (second < currentSecond) {
			// 過去へのseekなら最初からやり直してスキップ
			mBitmapDictionary.clear();
			mShapeDictionary.clear();
			mDisplayList.clear();
			mBackgroundColor = Color.BLACK;
			mSoundStreamHead = null;
			inSwf.seek(0L);
			readSWFFileHeaderWhole(inSwf);

			mFrameCount = 0;
		}

		RECORDHEADER tagBlock = new RECORDHEADER();
		while (!mPlayerParent.isFinish()) {
			// 切り上げ
			den = (mHeaderMovieSwf.frameRateIntegral << 8) | mHeaderMovieSwf.frameRateDecimal;
			num = (mFrameCount << 8) + den - 1;
			currentSecond = num / den;
			if (DEBUG_LOGV) {
				Log.v(LOG_TAG, Log.buf().append("seek current=").append(currentSecond)
						.append(" to=").append(second).toString());
			}
			if (second <= currentSecond) {
				break;
			}

			tagBlock.read(inSwf);
			if (isEndTag(tagBlock)) {
				break;
			}

			execTagBlock(tagBlock, inSwf, true);
		}
    }

    public interface SoundStreamListener {
    	public void onSoundStreamHead(
        		SoundStreamHead soundStreamHead,
        		SwfPlayer swfPlayer);
    	public void onSoundStreamBlock(
        		StreamSoundDataBase streamSoundData,
        		SoundStreamHead soundStreamHead,
        		SwfPlayer swfPlayer, boolean frameSkip);
    }

    public interface PlayerParent {
        boolean isFinish();
        boolean isPause();
        boolean isSeeking();
        int getSeekTimeBySecond();
        void notifySeekCompleted();
        void showFrame() throws FailAnalyzeSwfException, IOException;
        SwfPlayer.SoundStreamListener getSoundStreamListener();
    }
}
