/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package flash.swf;

import flash.swf.debug.DebugTable;
import flash.swf.tags.*;
import flash.swf.types.ButtonCondAction;
import flash.swf.types.ButtonRecord;
import flash.swf.types.CXForm;
import flash.swf.types.CXFormWithAlpha;
import flash.swf.types.CurvedEdgeRecord;
import flash.swf.types.FillStyle;
import flash.swf.types.Filter;
import flash.swf.types.FlashUUID;
import flash.swf.types.GlyphEntry;
import flash.swf.types.GradRecord;
import flash.swf.types.ImportRecord;
import flash.swf.types.KerningRecord;
import flash.swf.types.LineStyle;
import flash.swf.types.Matrix;
import flash.swf.types.MorphFillStyle;
import flash.swf.types.MorphGradRecord;
import flash.swf.types.MorphLineStyle;
import flash.swf.types.Rect;
import flash.swf.types.Shape;
import flash.swf.types.ShapeRecord;
import flash.swf.types.ShapeWithStyle;
import flash.swf.types.SoundInfo;
import flash.swf.types.StraightEdgeRecord;
import flash.swf.types.StyleChangeRecord;
import flash.swf.types.TextRecord;
import flash.swf.types.DropShadowFilter;
import flash.swf.types.BlurFilter;
import flash.swf.types.GlowFilter;
import flash.swf.types.BevelFilter;
import flash.swf.types.GradientGlowFilter;
import flash.swf.types.ConvolutionFilter;
import flash.swf.types.ColorMatrixFilter;
import flash.swf.types.GradientBevelFilter;
import flash.swf.types.Gradient;
import flash.swf.types.FocalGradient;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.zip.InflaterInputStream;

/**
 * A SWF tag decoder.  It is typically used by passing an InputStream
 * to the constructor and then calling parse() with a TagHandler.
 */
public final class TagDecoder
        implements TagValues
{
    public TagDecoder(InputStream swfIn)
	{
		this.swfIn = swfIn;
		this.swdIn = null;
	}

    public TagDecoder(InputStream swfIn, InputStream swdIn)
	{
		this.swfIn = swfIn;
		this.swdIn = swdIn;
	}

    public TagDecoder(InputStream in, URL swfUrl)
    {
        this.swfIn = in;
        this.swfUrl = swfUrl;
    }

    private Header header;
	private InputStream swfIn;
	private InputStream swdIn;
    private URL swfUrl;
    private DebugTable swd;
    private SwfDecoder r;
    private GenericTag jpegTables;
    private TagHandler handler;
    private boolean keepOffsets;

    private Dictionary dict = new Dictionary();

    /**
     * thrown by decoders when we have a fatal error.  Many errors
     * are not fatal.  In those cases, the error is reported but
     * parsing continues.
     */
	public static class FatalParseException extends Exception {

        private static final long serialVersionUID = 5819679367367802771L;}

    public void setKeepOffsets(boolean b)
    {
        keepOffsets = b;
    }

    /**
     * process the whole SWF stream, and close the input streams when finished.
     * @param handler
     * @throws IOException
     */
	public void parse(TagHandler handler) throws IOException
	{
        this.handler = handler;
        try
        {
            try
            {
                handler.setDecoderDictionary(dict);

                header = decodeHeader();
                handler.header(header);

                decodeTags(handler);
                handler.finish();
            }
            catch( FatalParseException e )
            {
                // errors already reported to TagHandler.
            }
            finally
            {
                if (swfIn != null)
                    swfIn.close();
            }
        }
        finally
        {
            if (swdIn != null)
                swdIn.close();
        }
	}

	public int getSwfVersion()
	{
		return header.version;
	}

    private void decodeTags(TagHandler handler) throws IOException
    {
        int type, h, length, currentOffset;

        do
        {
            currentOffset = r.getOffset();
            
            type = (h = r.readUI16()) >> 6;

            // is this a long tag header (>=63 bytes)?
            if (((length = h & 0x3F) == 0x3F))
            {
                // [ed] the player treats this as a signed field and stops if it is negative.
                length = r.readSI32();
                if (length < 0)
                {
                    handler.error("negative tag length: " + length + " at offset " + currentOffset);
                    break;
                }
            }
            int o = r.getOffset();
            int eat = 0;
            

            if (type != 0)
            {
                Tag t = decodeTag(type, length);
                if (r.getOffset() - o != length)
                {
                    handler.error("offset mismatch after " + Tag.names[t.code] + ": read " + (r.getOffset() - o) + ", expected " + length);
                    if (r.getOffset() - o < length)
                    {
                        eat = length - (r.getOffset() - o);
                        
                    }
                }
                handler.setOffsetAndSize(currentOffset, r.getOffset() - currentOffset);
                handler.any( t );
                t.visit(handler);
                if (eat > 0) // try to recover.  (flash 8 sometimes writes nonsense, usually in fonts)
                {
                    r.read( new byte[eat] );
                    
                }
            }
        }
        while (type != 0);
    }

    private Tag decodeTag(int type, int length) throws IOException
    {
        Tag t;
        int pos = r.getOffset();

        switch (type)
        {
        case stagProductInfo:
            t = decodeSerialNumber();
            break;
        case stagShowFrame:
            t = new ShowFrame();
            break;
        case stagMetadata:
            t = decodeMetadata();
            break;
        case stagDefineShape:
        case stagDefineShape2:
        case stagDefineShape3:
        case stagDefineShape4:
            t = decodeDefineShape(type);
            break;
        case stagPlaceObject:
            t = decodePlaceObject(length);
            break;
        case stagRemoveObject:
        case stagRemoveObject2:
            t = decodeRemoveObject(type);
            break;
        case stagDefineBinaryData:
            t = decodeDefineBinaryData(length);
            break;
        case stagDefineBits:
            t = decodeDefineBits(length);
            break;
        case stagDefineButton:
            t = decodeDefineButton(length);
            break;
        case stagJPEGTables:
            t = jpegTables = decodeJPEGTables(length);
            break;
        case stagSetBackgroundColor:
            t = decodeSetBackgroundColor();
            break;
        case stagDefineFont:
            t = decodeDefineFont();
            break;
        case stagDefineText:
        case stagDefineText2:
            t = decodeDefineText(type);
            break;
        case stagDoAction:
            t = decodeDoAction(length);
            break;
        case stagDefineFontInfo:
        case stagDefineFontInfo2:
            t = decodeDefineFontInfo(type, length);
            break;
        case stagDefineSound:
            t = decodeDefineSound(length);
            break;
        case stagStartSound:
            t = decodeStartSound();
            break;
        case stagDefineButtonSound:
            t = decodeDefineButtonSound();
            break;
        case stagSoundStreamHead2:
        case stagSoundStreamHead:
            t = decodeSoundStreamHead(type);
            break;
        case stagSoundStreamBlock:
            t = decodeSoundStreamBlock(length);
            break;
        case stagDefineBitsLossless:
            t = decodeDefineBitsLossless(length);
            break;
        case stagDefineBitsJPEG2:
            t = decodeDefineJPEG2(length);
            break;
        case stagDefineButtonCxform:
            t = decodeDefineButtonCxform();
            break;
        case stagProtect:
            t = decodeProtect(length);
            break;
        case stagPlaceObject2:
            t = decodePlaceObject23(stagPlaceObject2, length);
            break;
        case stagPlaceObject3:
            t = decodePlaceObject23(stagPlaceObject3, length);
            break;
        case stagDefineButton2:
            t = decodeDefineButton2(length);
            break;
        case stagDefineBitsJPEG3:
            t = decodeDefineJPEG3(length);
            break;
        case stagDefineBitsLossless2:
            t = decodeDefineBitsLossless2(length);
            break;
        case stagDefineEditText:
            t = decodeDefineEditText();
            break;
        case stagDefineSprite:
            t = decodeDefineSprite(pos+length);
            break;
        case stagDefineScalingGrid:
            t = decodeDefineScalingGrid();
            break;
        case stagFrameLabel:
            t = decodeFrameLabel(length);
            break;
        case stagDefineMorphShape:
            t = decodeDefineMorphShape();
            break;
        case stagDefineMorphShape2:
            t = decodeDefineMorphShape2();
            break;
        case stagDefineFont2:
            t = decodeDefineFont2();
            break;
        case stagDefineFont3:
            t = decodeDefineFont3();
            break;
        case stagDefineFont4:
            t = decodeDefineFont4(length);
            break;
        case stagExportAssets:
            t = decodeExportAssets();
            break;
        case stagImportAssets:
        case stagImportAssets2:
            t = decodeImportAssets(type);
            break;
        case stagEnableDebugger2:
        case stagEnableDebugger:
            t = decodeEnableDebugger(type);
            break;
        case stagDoInitAction:
            t = decodeDoInitAction(length);
            break;
        case stagDefineVideoStream:
            t = decodeDefineVideoStream();
            break;
        case stagVideoFrame:
            t = decodeVideoFrame(length);
            break;
        case stagDebugID:
            t = decodeDebugID(type, length);
            break;
        case stagScriptLimits:
            t = decodeScriptLimits();
            break;
        case stagSetTabIndex:
            t = decodeSetTabIndex();
            break;
        case stagDoABC:
        case stagDoABC2:
            t = decodeDoABC(type, length);
            break;
        case stagSymbolClass:
            t = decodeSymbolClass();
            break;
        case stagFileAttributes:
            t = decodeFileAttributes();
            break;
        case stagEnableTelemetry:
            t = decodeEnableTelemetry();
            break;
        case stagDefineFontAlignZones:
            t = decodeDefineFontAlignZones();
            break;
        case stagCSMTextSettings:
            t = decodeCSMTextSettings();
            break;
        case stagDefineSceneAndFrameLabelData:
            t = decodeDefineSceneAndFrameData(length);
            break;
        case stagDefineFontName:
            t = decodeDefineFontName();
            break;
        default:
            t = decodeUnknown(length, type);
            break;
        }

        int consumed = r.getOffset() - pos;

        // [preilly] It looks like past Authoring tools have generated some SWF's with
        // stagSoundStreamHead tags of length 4 with compression set to mp3, but the tag
        // really has 6 bytes in it and the player always reads the 6 bytes, so ignore the
        // difference between the consumed and the length for this special case.
        if ((consumed != length) && (type == stagSoundStreamHead) && (consumed != (length + 2)))
        {
            throw new SwfFormatException(TagValues.names[type] + " at pos "+pos+ ": " + consumed +
                                  " bytes were read. " + length + " byte were required.");
        }
        return t;
    }

    private Tag decodeDefineSceneAndFrameData(int length) throws IOException
    {
        DefineSceneAndFrameLabelData t = new DefineSceneAndFrameLabelData();
        t.data = new byte[length];
        r.readFully(t.data);
        return t;
    }

    private Tag decodeDoABC(int type, int length) throws IOException
	{
		DoABC t;

        if (type == stagDoABC2)
        {
            int pos = r.getOffset();
            int skip = r.readSI32();
            String name = r.readString();
            t = new DoABC( name , skip );
            // cannot just use length of string, because might not match
            // the number of bytes in the string for nonascii characters
            //length -= (4 + name.length() + 1);
            length -= (r.getOffset() - pos);
        }
        else
        {
            t = new DoABC();
        }

		t.abc = new byte[length];
		r.readFully(t.abc);
		return t;
	}

	private Tag decodeSymbolClass() throws IOException
	{
		SymbolClass t = new SymbolClass();
		int count = r.readUI16();

		t.class2tag = new HashMap<String, Tag>(count);

		for (int i=0; i < count; i++)
		{
			int idref = r.readUI16();
			String name = r.readString();
			if (idref == 0)
			{
				t.topLevelClass = name;
				continue;
			}
			DefineTag ref = dict.getTag(idref);
			t.class2tag.put(name, ref);
			if (ref.name != null)
			{
				if (!ref.name.equals(name))
				{
					handler.error("SymbolClass: symbol " + idref + " already exported as " + ref.name);
				}
				//else
				//{
                    // FIXME: is this right?  seem to be getting redundant message in swfdumps that work right in the player
                    // FIXME: We should eventually enforce that export names not be used in zaphod movies,
                    // FIXME: but in the short term, all this message means is that the symbol was both exported
                    // FIXME: via ExportAssets and also associated with a class via SymbolClass.  They have
                    // FIXME: different semantic meanings, so this error is a bit off-base.  --rg 
				//	handler.error("Redundant SymbolClass of " + ref.name + ".  Found " + ref.getClass().getName() + " of same name in dictionary.");
				//}
			}
			else
			{
				DefineTag other = dict.getTag(name);
				if (other != null)
				{
					int id = dict.getId(other);
					handler.error("Symbol " + name + " already refers to ID " + id);
				}
				ref.name = name;
				dict.addName(ref, name);
			}
		}
		return t;
	}

    private Tag decodeSetTabIndex() throws IOException
    {
        int depth = r.readUI16();
        int index = r.readUI16();
        return new SetTabIndex(depth, index);
    }

    private Tag decodeUnknown(int length, int code) throws IOException
    {
        GenericTag t;
        t = new GenericTag(code);
        t.data = new byte[length];
        r.readFully(t.data);
        return t;
    }

    private ScriptLimits decodeScriptLimits() throws IOException
    {
        ScriptLimits scriptLimits = new ScriptLimits(r.readUI16(), r.readUI16());
        return scriptLimits;
    }

    private Tag decodeDebugID(int type, int length) throws IOException
    {
        DebugID t;
        t = new DebugID(type);
        t.uuid = decodeFlashUUID(length);

		if (swdIn != null)
		{
			InputStream in = swdIn;
            DebugTable swd = new DebugTable();
            new DebugDecoder(in).readSwd(swd);

            if (!t.uuid.equals(swd.uuid))
            {
                handler.error("SWD uuid "+swd.uuid+" doesn't match "+t.uuid);
            }
            else if (swd.version != getSwfVersion())
            {
                handler.error("SWD version number "+swd.version+" doesn't match SWF version number "+getSwfVersion());
            }
            else
            {
                this.swd = swd;
            }
		}
        else if (swfUrl != null)
        {
            // look for a SWD file in the same place the player would look
            String path = swfUrl.toString();

			int q = path.indexOf("?");
			String query = null;
            if (q != -1)
			{
				query = path.substring(q);
                path = path.substring(0, q);
			}

			URL swdUrl;
            if (path.endsWith(".swf"))
            {
                path = path.substring(0,path.length()-4)+".swd";
            }
            else
            {
                path = path + ".swd";
            }

            if (query != null)
            {
                path = path + query;
            }

            swdUrl = new URL(path);

            try
            {
                InputStream in = swdUrl.openStream();
                DebugTable swd = new DebugTable();
                new DebugDecoder(in).readSwd(swd);

                if (!t.uuid.equals(swd.uuid))
                {
                    handler.error("SWD uuid "+swd.uuid+" doesn't match "+t.uuid);
                }
				else if (swd.version != getSwfVersion())
				{
					handler.error("SWD version number "+swd.version+" doesn't match SWF version number "+getSwfVersion());
				}
                else
                {
                    this.swd = swd;
                }
            }
            catch (FileNotFoundException ex)
            {
                handler.error("SWD not found at url " + swdUrl);
            }
        }

        return t;
    }

    private FlashUUID decodeFlashUUID(int length) throws IOException
    {
        byte[] uuid = new byte[length];
        r.readFully(uuid);
        return new FlashUUID(uuid);
    }

    private Tag decodeVideoFrame(int length) throws IOException
    {
        VideoFrame t;
        t = new VideoFrame();
        int pos = r.getOffset();

        int idref = r.readUI16();
        t.stream = (DefineVideoStream) dict.getTag(idref);
        t.frameNum = r.readUI16();

        length -= r.getOffset() - pos;

        t.videoData = new byte[length];
        r.readFully(t.videoData);
        return t;
    }

    private Tag decodeDefineVideoStream() throws IOException
    {
        DefineVideoStream t;
        t = new DefineVideoStream();
        int id = r.readUI16();
        t.numFrames = r.readUI16();
        t.width = r.readUI16();
        t.height = r.readUI16();

        r.syncBits();

        r.readUBits(4); // reserved
        t.deblocking = r.readUBits(3);
        t.smoothing = r.readBit();

        t.codecID = r.readUI8();

        dict.add(id, t);
        return t;
    }

    private Tag decodeDoInitAction(int length) throws IOException
    {
        DoInitAction t;
        t = new DoInitAction();
        int idref = r.readUI16();
        try
        {
            t.sprite = (DefineSprite) dict.getTag(idref);
            if (t.sprite.initAction != null)
            {
                handler.error("Sprite " + idref + " initaction redefined");
            }
            else
            {
                t.sprite.initAction = t;
            }
        }
        catch (IllegalArgumentException e)
        {
            handler.error(e.getMessage());
        }
        ActionDecoder actionDecoder = new ActionDecoder(r,swd);
        actionDecoder.setKeepOffsets(keepOffsets);
        t.actionList = actionDecoder.decode(length-2);
        return t;
    }

    private Tag decodeEnableDebugger(int code) throws IOException
    {
        EnableDebugger t;
        t = new EnableDebugger(code);
        if (code == stagEnableDebugger2)
        {
            if (getSwfVersion() < 6)
                handler.error("EnableDebugger2 not valid before SWF 6");
            t.reserved = r.readUI16(); // reserved
        }
        t.password = r.readString();
        return t;
    }

    private Tag decodeImportAssets(int code) throws IOException
    {
        ImportAssets t;
        t = new ImportAssets(code);

        t.url = r.readString();
        if (code == stagImportAssets2)
        {
        	t.downloadNow = (r.readUI8() == 1);
        	if (r.readUI8() == 1) // hasDigest == 1
        	{
        		t.SHA1 = new byte[20];
        		r.readFully(t.SHA1);
        	}
        }
        
        int count = r.readUI16();
        t.importRecords = new ArrayList<ImportRecord>();

        for (int i=0; i < count; i++)
        {
            ImportRecord ir = new ImportRecord();
            int id = r.readUI16();
            ir.name = r.readString();
            t.importRecords.add(ir);
            dict.add(id, ir);
            dict.addName(ir, ir.name);
        }
        return t;
    }

    private Tag decodeExportAssets() throws IOException
    {
        ExportAssets t;
        t = new ExportAssets();

        int count = r.readUI16();

        t.exports = new ArrayList<Tag>(count);

        for (int i=0; i < count; i++)
        {
            int idref = r.readUI16();
            String name = r.readString();
            DefineTag ref = dict.getTag(idref);
            t.exports.add(ref);
            if (ref.name != null)
            {
                if (!ref.name.equals(name))
                    handler.error("ExportAsset: symbol " + idref + " already exported as " + ref.name);
                else
                    handler.error("redundant export of "+ref.name);
            }
            else
            {
                DefineTag other = dict.getTag(name);
                if (other != null)
                {
                    int id = dict.getId(other);
                    handler.error("Symbol "+name+" already refers to ID "+id);
                }
                ref.name = name;
                dict.addName(ref, name);
            }
        }
        return t;
    }

    private Tag decodeDefineFont2() throws IOException
    {
        DefineFont2 t = new DefineFont2();
        return decodeDefineFont2And3(t);
    }

    private Tag decodeDefineFont3() throws IOException
    {
        DefineFont3 t = new DefineFont3();
        return decodeDefineFont2And3(t);
    }

    private Tag decodeDefineFont2And3(DefineFont2 t) throws IOException
    {
        int id = r.readUI16();

        r.syncBits();

        t.hasLayout = r.readBit();
        t.shiftJIS = r.readBit();
        t.smallText = r.readBit();
        t.ansi = r.readBit();
        t.wideOffsets = r.readBit();
        t.wideCodes = r.readBit();
        // enable after we're sure that bug 147073 isn't a bug.  If it is a bug, then this can be
        // removed as well as the stageDefineFont3-specific code in TagEncoder.defineFont2()
        //if (t.code == stagDefineFont3 && ! t.wideCodes)
        //{
        //    handler.error("widecodes must be true in DefineFont3");
        //}

        t.italic = r.readBit();
        t.bold = r.readBit();

        t.langCode = r.readUI8();

        t.fontName = r.readLengthString();

        int numGlyphs = r.readUI16();

        long[] offsets = new long[numGlyphs];
        for (int i = 0; i < numGlyphs; i++)
        {
            if (t.wideOffsets)
                offsets[i] = r.readUI32();
            else
                offsets[i] = r.readUI16();
        }

        long codeTableOffset = 0;
        if (numGlyphs > 0)
        {
	        if (t.wideOffsets)
	            codeTableOffset = r.readUI32();
	        else
	            codeTableOffset = r.readUI16();
        }

        t.glyphShapeTable = new Shape[numGlyphs];

        for (int i = 0; i < numGlyphs; i++)
        {
            int glyphLength;
            if (i < (numGlyphs - 1))
                glyphLength = (int)(offsets[i+1] - offsets[i]);
            else
                glyphLength = (int)(codeTableOffset - offsets[i]);

            t.glyphShapeTable[i] = decodeGlyph(stagDefineShape3, glyphLength);
        }

        t.codeTable = new char[numGlyphs];
        if (t.wideCodes)
        {
            for (int i = 0; i < numGlyphs; i++)
                t.codeTable[i] = (char) r.readUI16();
        }
        else
        {
            for (int i = 0; i < numGlyphs; i++)
                t.codeTable[i] = (char) r.readUI8();
        }

        if (t.hasLayout)
        {
            t.ascent = r.readSI16();
            t.descent = r.readSI16();
            t.leading = r.readSI16();
            t.advanceTable = new short[numGlyphs];
            for (int i = 0; i < numGlyphs; i++)
            {
                t.advanceTable[i] = (short)r.readSI16();
            }
            t.boundsTable = new Rect[numGlyphs];
            for (int i = 0; i < numGlyphs; i++)
            {
                t.boundsTable[i] = decodeRect();
            }

            t.kerningCount = r.readUI16();
            t.kerningTable = new KerningRecord[t.kerningCount];
            for (int i = 0; i < t.kerningCount; i++)
            {
                t.kerningTable[i] = decodeKerningRecord(t.wideCodes);
            }
        }

        dict.add(id, t);
        dict.addFontFace( t );
        return t;
    }

    private Tag decodeDefineFont4(int length) throws IOException
    {
        DefineFont4 t = new DefineFont4();
        int pos = r.getOffset();

        int id = r.readUI16();

        r.syncBits();
        r.readUBits(5); // reserved
        t.hasFontData = r.readBit();
        //t.smallText = r.readBit();
        t.italic = r.readBit();
        t.bold = r.readBit();

        //t.langCode = r.readUI8();
        t.fontName = r.readString();

        if (t.hasFontData)
        {
            length -= r.getOffset() - pos;
            t.data = new byte[length];
            r.readFully(t.data);
        }

        dict.add(id, t);
        dict.addFontFace(t);
        return t;
    }
    
    private KerningRecord decodeKerningRecord(boolean wideCodes) throws IOException
    {
        KerningRecord kr = new KerningRecord();

        kr.code1 = (wideCodes) ? r.readUI16() : r.readUI8();
        kr.code2 = (wideCodes) ? r.readUI16() : r.readUI8();
        kr.adjustment = r.readUI16();

        return kr;
    }

    private Tag decodeDefineMorphShape() throws IOException
    {
    	return decodeDefineMorphShape(stagDefineMorphShape);
    }

    private Tag decodeDefineMorphShape2() throws IOException
    {
    	return decodeDefineMorphShape(stagDefineMorphShape2);
    }

    private Tag decodeDefineMorphShape(int code) throws IOException
    {
        DefineMorphShape t = new DefineMorphShape(code);
        int id = r.readUI16();
        t.startBounds = decodeRect();
        t.endBounds = decodeRect();
        if (code == stagDefineMorphShape2)
        {
        	t.startEdgeBounds = decodeRect();
        	t.endEdgeBounds = decodeRect();
            r.readUBits(6);
            t.usesNonScalingStrokes = r.readBit();
            t.usesScalingStrokes = r.readBit();
        }
        int offset = (int)r.readUI32(); // offset to EndEdges
        t.fillStyles = decodeMorphFillstyles(code);
        t.lineStyles = decodeMorphLinestyles(code);
        t.startEdges = decodeShape(stagDefineShape3);
        if (offset != 0)
            t.endEdges = decodeShape(stagDefineShape3);
        dict.add(id, t);
        return t;
    }

    private MorphLineStyle[] decodeMorphLinestyles(int code) throws IOException
    {
        int count = r.readUI8();
        if (count == 0xFF)
        {
            count = r.readUI16();
        }

        MorphLineStyle[] styles = new MorphLineStyle[count];

        for (int i = 0; i < count; i++)
        {
            MorphLineStyle s = new MorphLineStyle();
            s.startWidth = r.readUI16();
            s.endWidth = r.readUI16();
            if (code == stagDefineMorphShape2)
            {
            	s.startCapsStyle = r.readUBits(2);
            	s.jointStyle = r.readUBits(2);
            	s.hasFill = r.readBit();
            	s.noHScale = r.readBit();
            	s.noVScale = r.readBit();
            	s.pixelHinting = r.readBit();
            	r.readUBits(5); // reserved
            	s.noClose = r.readBit();
            	s.endCapsStyle = r.readUBits(2);
            	if (s.jointStyle == 2)
            	{
            		s.miterLimit = r.readUI16();
            	}
            }
            if (!s.hasFill)
            {
            	s.startColor = decodeRGBA(r);
                s.endColor = decodeRGBA(r);
            }
            if (s.hasFill)
            {
            	s.fillType = decodeMorphFillStyle(code);
            }

            styles[i] = s;
        }

        return styles;
    }

    private MorphFillStyle[] decodeMorphFillstyles(int shape) throws IOException
    {
        int count = r.readUI8();
        if (count == 0xFF)
        {
            count = r.readUI16();
        }

        MorphFillStyle[] styles = new MorphFillStyle[count];

        for (int i = 0; i < count; i++)
        {
            styles[i] = decodeMorphFillStyle(shape);
        }

        return styles;
    }

    private MorphFillStyle decodeMorphFillStyle(int shape) throws IOException
    {
        MorphFillStyle s = new MorphFillStyle();

        s.type = r.readUI8();
        switch (s.type)
        {
        case FillStyle.FILL_SOLID: // 0x00
            s.startColor = decodeRGBA(r);
            s.endColor = decodeRGBA(r);
            break;
        case FillStyle.FILL_GRADIENT: // 0x10 linear gradient fill
        case FillStyle.FILL_RADIAL_GRADIENT: // 0x12 radial gradient fill
        case FillStyle.FILL_FOCAL_RADIAL_GRADIENT: // 0x13 focal radial gradient fill
            s.startGradientMatrix = decodeMatrix();
            s.endGradientMatrix = decodeMatrix();
            s.gradRecords = decodeMorphGradient();
            if (s.type == FillStyle.FILL_FOCAL_RADIAL_GRADIENT && shape == stagDefineMorphShape2)
            {
            	s.ratio1 = r.readSI16();
            	s.ratio2 = r.readSI16();
            }
            break;
        case FillStyle.FILL_BITS: // 0x40 tiled bitmap fill
        case (FillStyle.FILL_BITS | FillStyle.FILL_BITS_CLIP): // 0x41 clipped bitmap fill
        case (FillStyle.FILL_BITS | FillStyle.FILL_BITS_NOSMOOTH): // 0x42 tiled non-smoothed fill
        case (FillStyle.FILL_BITS | FillStyle.FILL_BITS_CLIP | FillStyle.FILL_BITS_NOSMOOTH): // 0x43 clipped non-smoothed fill
            int idref = r.readUI16();
            try
            {
                s.bitmap = dict.getTag(idref);
            }
            catch (IllegalArgumentException ex)
            {
                handler.error(ex.getMessage());
                s.bitmap = null;
            }
            s.startBitmapMatrix = decodeMatrix();
            s.endBitmapMatrix = decodeMatrix();
            break;
        default:
            throw new SwfFormatException("unrecognized fill style type: " + s.type);
        }

        return s;
    }

    private MorphGradRecord[] decodeMorphGradient() throws IOException
    {
        int num = r.readUI8();
        MorphGradRecord[] gradRecords = new MorphGradRecord[num];

        for (int i = 0; i < num; i++)
        {
            MorphGradRecord g = new MorphGradRecord();
            g.startRatio = r.readUI8();
            g.startColor = decodeRGBA(r);
            g.endRatio = r.readUI8();
            g.endColor = decodeRGBA(r);

            gradRecords[i] = g;
        }

        return gradRecords;
    }

    private Tag decodeFrameLabel(int length) throws IOException
    {
        FrameLabel t = new FrameLabel();
        int pos = r.getOffset();
        t.label = r.readString();
        if (getSwfVersion() >= 6)
        {
            if (length - r.getOffset() + pos == 1)
            {
                int anchor = r.readUI8();
                if (anchor != 0 && anchor != 1)
                    handler.error("illegal anchor value: "+anchor+".  Must be 0 or 1");
                // player treats any nonzero value as true
                t.anchor = (anchor != 0);
            }
        }
        return t;
    }

    private Tag decodeDefineEditText() throws IOException
    {
        DefineEditText t;
        t = new DefineEditText();
        int id = r.readUI16();
        t.bounds = decodeRect();

        r.syncBits();

        t.hasText = r.readBit();
        t.wordWrap = r.readBit();
        t.multiline = r.readBit();
        t.password = r.readBit();
        t.readOnly = r.readBit();
        t.hasTextColor = r.readBit();
        t.hasMaxLength = r.readBit();
        t.hasFont = r.readBit();
        t.hasFontClass = r.readBit(); // FP 9.0.45 or later
        t.autoSize = r.readBit();
        t.hasLayout = r.readBit();
        t.noSelect = r.readBit();
        t.border = r.readBit();
        t.wasStatic = r.readBit();
        t.html = r.readBit();
        t.useOutlines = r.readBit();

        if (t.hasFont)
        {
            int idref = r.readUI16();
            t.font = (DefineFont) dict.getTag(idref);
            t.height = r.readUI16();
        }

        if (t.hasFontClass)
        {
            t.fontClass = r.readString();
            t.height = r.readUI16();
        }

        if (t.hasTextColor)
        {
            t.color = decodeRGBA(r);
        }

        if (t.hasMaxLength)
        {
            t.maxLength = r.readUI16();
        }

        if (t.hasLayout)
        {
            t.align = r.readUI8();
            t.leftMargin = r.readUI16();
            t.rightMargin = r.readUI16();
            t.ident = r.readUI16();
            t.leading = r.readSI16(); // see errata, leading is signed
        }

        t.varName = r.readString();

        if (t.hasText)
        {
            t.initialText = r.readString();
        }

        dict.add(id, t);
        return t;
    }

    private Tag decodeDefineScalingGrid() throws IOException
    {
        DefineScalingGrid t = new DefineScalingGrid();
        int idref = r.readUI16();
        try
        {
            t.scalingTarget = dict.getTag(idref);
            if (t.scalingTarget instanceof DefineSprite)
            {
                DefineSprite targetSprite = (DefineSprite) t.scalingTarget;
                if (targetSprite.scalingGrid != null)
                {
                    handler.error("Sprite " + idref + " scaling grid redefined" );
                }
                targetSprite.scalingGrid = t;
            }
            else if (t.scalingTarget instanceof DefineButton)
            {
                DefineButton targetButton = (DefineButton) t.scalingTarget;
                if (targetButton.scalingGrid != null)
                {
                    handler.error("Button " + idref + " scaling grid redefined");
                }
                targetButton.scalingGrid = t;
            }
        }
        catch (Exception e)
        {
            return null;
        }
        t.rect = decodeRect();
        return t;
    }

    private Tag decodeDefineBitsLossless2(int length) throws IOException
    {
        DefineBitsLossless t;
        t = new DefineBitsLossless(stagDefineBitsLossless2);
        SwfDecoder r1 = r;

        int pos = r1.getOffset();

        int id = r1.readUI16();
        t.format = r1.readUI8();
        t.width = r1.readUI16();
        t.height = r1.readUI16();

        byte[] data;

        switch (t.format)
        {
        case 3:
            int colorTableSize = r1.readUI8()+1;
            length -= r1.getOffset() - pos;
            data = new byte[length];
            r1.readFully(data);
            r1 = new SwfDecoder(new InflaterInputStream(new ByteArrayInputStream(data)), getSwfVersion());
            decodeAlphaColorMapData(r1, t, colorTableSize);
            break;
        case 4:
        case 5:
            length -= r1.getOffset() - pos;
            data = new byte[length];
            r1.readFully(data);
            r1 = new SwfDecoder(new InflaterInputStream(new ByteArrayInputStream(data)), getSwfVersion());
            t.data = new byte[t.width * t.height * 4];
            r1.readFully(t.data);
            break;
        default:
            throw new SwfFormatException("Illegal bitmap format " + t.format);
        }

        dict.add(id, t);
        return t;
    }

    private void decodeAlphaColorMapData(SwfDecoder r1, DefineBitsLossless tag, int tableSize) throws IOException
    {
        int width = tag.width;
        int height = tag.height;

        tag.colorData = new int[tableSize];

        for (int i = 0; i < tableSize; i++)
        {
            tag.colorData[i] = decodeRGBA(r1);
        }

        if (width % 4 != 0)
        {
            width = (width / 4 + 1) * 4;
        }

        int data_size = width * height;

        tag.data = new byte[data_size];
        //r1.read(tag.data);

        int i = 0;
        int b;
        while (i < data_size)
        {
            b = r1.readUI8();
            if (b != -1)
            {
                tag.data[i] = (byte) b;
                i++;
            }
            else
            {
                break;
            }
        }

        int extra = 0;
        while (r1.readUI8() != -1)
        {
            extra++;
        }

        if (extra > 0)
        {
            throw new SwfFormatException(extra + " bytes of bitmap data (" + width + "x" + height + ") not read!");
        }
        else if (i != data_size)
        {
            throw new SwfFormatException("(" + width + "x" + height + ") data buffer " + (data_size - i) + " bytes too big...");
        }
    }

    private Tag decodeDefineJPEG3(int length) throws IOException
    {
        DefineBitsJPEG3 t;
        t = new DefineBitsJPEG3();
        int pos = r.getOffset();
        int id = r.readUI16();
        t.alphaDataOffset = r.readUI32();

        t.data = new byte[(int) t.alphaDataOffset];
        r.readFully(t.data);

        length -= r.getOffset() - pos;
        byte[] temp = new byte[length];
        r.readFully(temp);

        SwfDecoder r1 = new SwfDecoder(new InflaterInputStream(new ByteArrayInputStream(temp)), getSwfVersion());

        int alpha, i = 0;
        byte[] alphaData = new byte[length];

        while ((alpha = r1.readUI8()) != -1)
        {
            if (i == alphaData.length)
            {
                byte[] b = new byte[i + length];
                System.arraycopy(alphaData, 0, b, 0, alphaData.length);
                alphaData = b;
            }
            alphaData[i] = (byte)alpha;
            i++;
        }

        t.alphaData = new byte[i];
        System.arraycopy(alphaData, 0, t.alphaData, 0, i);

        dict.add(id, t);
        return t;
    }

    private Tag decodeDefineButton2(int length) throws IOException
    {
		int endpos = r.getOffset()+length;
        DefineButton t = new DefineButton(stagDefineButton2);

        int id = r.readUI16();

        r.syncBits();
        r.readUBits(7); // reserved
        t.trackAsMenu = r.readBit();

        int actionOffset = r.readUI16();

        // read button data
        ArrayList<Object> list = new ArrayList<Object>(5);
        ButtonRecord record;
        while ((record = decodeButtonRecord(t.code)) != null)
        {
            list.add(record);
        }

        t.buttonRecords = new ButtonRecord[list.size()];
        list.toArray(t.buttonRecords);
        list.clear();

        if (actionOffset > 0)
        {
            list = new ArrayList<Object>();

            int pos = r.getOffset();
            while ((actionOffset = r.readUI16()) > 0)
            {
                list.add(decodeButtonCondAction(actionOffset-2));
                if (r.getOffset() != pos+actionOffset)
                {
                    throw new SwfFormatException("incorrect offset read in ButtonCondAction. read "+actionOffset+"");
                }
                pos = r.getOffset();
            }
			// actionOffset == 0 means this will be the last record
            list.add(decodeButtonCondAction(endpos-r.getOffset()));

            t.condActions = new ButtonCondAction[list.size()];
            list.toArray(t.condActions);
        }
        else
        {
            t.condActions = new ButtonCondAction[0];
        }
        while (r.getOffset() < endpos)
        {
            int b = r.readUI8();
            if (b != 0)
            {
                throw new SwfFormatException("nonzero data past end of DefineButton2");
            }
        }

        dict.add(id, t);
        return t;
    }

    private ButtonCondAction decodeButtonCondAction(int length) throws IOException
    {
        ButtonCondAction a = new ButtonCondAction();
        r.syncBits();
        a.keyPress = r.readUBits(7);
        a.overDownToIdle = r.readBit();

        a.idleToOverDown = r.readBit();
        a.outDownToIdle = r.readBit();
        a.outDownToOverDown = r.readBit();
        a.overDownToOutDown = r.readBit();
        a.overDownToOverUp = r.readBit();
        a.overUpToOverDown = r.readBit();
        a.overUpToIdle = r.readBit();
        a.idleToOverUp = r.readBit();

        ActionDecoder actionDecoder = new ActionDecoder(r,swd);
        actionDecoder.setKeepOffsets(keepOffsets);
        a.actionList = actionDecoder.decode(length-2);

        return a;
    }

    private Tag decodePlaceObject23(int type, int length) throws IOException
    {
        PlaceObject t = new PlaceObject(type);
        int pos = r.getOffset();
        t.flags = r.readUI8();
        if (type == stagPlaceObject3)
        {
            t.flags2 = r.readUI8();
        }
        t.depth = r.readUI16();
        if (t.hasClassName())
        {
            t.className = r.readString();
        }
        if (t.hasCharID())
        {
            int idref = r.readUI16();
            t.setRef(dict.getTag(idref));
        }
        if (t.hasMatrix())
        {
            t.matrix = decodeMatrix();
        }
        if (t.hasCxform())
        {
            t.setCxform(decodeCxforma());
        }
        if (t.hasRatio())
        {
            t.ratio = r.readUI16();
        }
        if (t.hasName())
        {
            t.name = r.readString();
        }
        if (t.hasClipDepth())
        {
            t.clipDepth = r.readUI16();
        }
        if (type == stagPlaceObject3)
        {
            if (t.hasFilterList())
            {
                t.filters = decodeFilterList();
            }
            if (t.hasBlendMode())
            {
                t.blendMode = r.readUI8();
            }
        }
        if (t.hasClipAction())
        {
            ActionDecoder actionDecoder = new ActionDecoder(r,swd);
            actionDecoder.setKeepOffsets(keepOffsets);
            t.clipActions = actionDecoder.decodeClipActions(length - (r.getOffset() - pos));
        }
        return t;
    }

    private List<Filter> decodeFilterList() throws IOException
    {
        LinkedList<Filter> filters = new LinkedList<Filter>();
        int count = r.readUI8();
        for (int i = 0; i < count; ++i)
        {
            int filterID = r.readUI8();
            switch( filterID )
            {
            // NOTE: the filter decoding is pretty much just "save enough bits to regenerate", and ignores
            // the real formatting of the filters (i.e. fixed 8.8 types, etc.)  If you need the actual
            // values rather than just acting as a passthrough, you will need to enhance the types.

                case DropShadowFilter.ID:    filters.add( decodeDropShadowFilter() );    break;
                case BlurFilter.ID:          filters.add( decodeBlurFilter() );          break;
                case GlowFilter.ID:          filters.add( decodeGlowFilter() );          break;
                case BevelFilter.ID:         filters.add( decodeBevelFilter() );         break;
                case GradientGlowFilter.ID:  filters.add( decodeGradientGlowFilter() );  break;
                case ConvolutionFilter.ID:   filters.add( decodeConvolutionFilter() );   break;
                case ColorMatrixFilter.ID:   filters.add( decodeColorMatrixFilter() );   break;
                case GradientBevelFilter.ID: filters.add( decodeGradientBevelFilter() ); break;
            }
        }
        return filters;
    }

    private DropShadowFilter decodeDropShadowFilter() throws IOException
    {
        DropShadowFilter f = new DropShadowFilter();
        f.color = decodeRGBA( r );
        f.blurX = r.readSI32();
        f.blurY = r.readSI32();
        f.angle = r.readSI32();
        f.distance = r.readSI32();
        f.strength = r.readUI16();  // really fixed8
        f.flags = r.readUI8();
        return f;
    }
    private BlurFilter decodeBlurFilter() throws IOException
    {
        BlurFilter f = new BlurFilter();
        f.blurX = r.readSI32(); // FIXED
        f.blurY = r.readSI32();
        f.passes = r.readUI8();
        return f;
    }

    private GlowFilter decodeGlowFilter() throws IOException
    {
        GlowFilter f = new GlowFilter();
        f.color = decodeRGBA( r );
        f.blurX = r.readSI32();
        f.blurY = r.readSI32();
        f.strength = r.readUI16();          // fixed 8.8
        f.flags = r.readUI8();  // bunch of fields
        return f;
    }
    private BevelFilter decodeBevelFilter() throws IOException
    {
        BevelFilter f = new BevelFilter();
        f.highlightColor = decodeRGBA(r);
        f.shadowColor = decodeRGBA(r);
        f.blurX = r.readSI32();
        f.blurY = r.readSI32();
        f.angle = r.readSI32();
        f.distance = r.readSI32();
        f.strength = r.readUI16();  // fixed 8.8
        f.flags = r.readUI8();  // bunch of fields
        return f;
    }
    private GradientGlowFilter decodeGradientGlowFilter() throws IOException
    {
        GradientGlowFilter f = new GradientGlowFilter();
        f.numcolors = r.readUI8();
        f.gradientColors = new int[f.numcolors];
        for (int i = 0; i < f.numcolors; ++i)
            f.gradientColors[i] = decodeRGBA( r );
        f.gradientRatio = new int[f.numcolors];
        for (int i = 0; i < f.numcolors; ++i)
            f.gradientRatio[i] = r.readUI8();
//        f.color = decodeRGBA( r );
        f.blurX = r.readSI32();
        f.blurY = r.readSI32();
        f.angle = r.readSI32();
        f.distance = r.readSI32();
        f.strength = r.readUI16();  // fixed 8.8
        f.flags = r.readUI8();  // bunch of fields

        return f;
    }
    private ConvolutionFilter decodeConvolutionFilter() throws IOException
    {
        ConvolutionFilter f = new ConvolutionFilter();
        f.matrixX = r.readUI8();
        f.matrixY = r.readUI8();
        f.divisor = r.readFloat();
        f.bias = r.readFloat();
        f.matrix = new float[f.matrixX*f.matrixY];
        for (int i = 0; i <f.matrixX*f.matrixY; ++i)
            f.matrix[i] = r.readFloat();
        f.color = decodeRGBA( r );
        f.flags = r.readUI8();
        return f;
    }

    private ColorMatrixFilter decodeColorMatrixFilter() throws IOException
    {
        ColorMatrixFilter f = new ColorMatrixFilter();

        for (int i = 0; i < 20; ++i)
        {
            f.values[i] = r.readFloat();
        }
        return f;
    }

    private GradientBevelFilter decodeGradientBevelFilter() throws IOException
    {
        GradientBevelFilter f = new GradientBevelFilter();
        f.numcolors = r.readUI8();
        f.gradientColors = new int[f.numcolors];
        for (int i = 0; i < f.numcolors; ++i)
            f.gradientColors[i] = decodeRGBA( r );
        f.gradientRatio = new int[f.numcolors];
        for (int i = 0; i < f.numcolors; ++i)
            f.gradientRatio[i] = r.readUI8();
//        f.shadowColor = decodeRGBA( r );
//        f.highlightColor = decodeRGBA( r );
        f.blurX = r.readSI32();
        f.blurY = r.readSI32();
        f.angle = r.readSI32();
        f.distance = r.readSI32();
        f.strength = r.readUI16();
        f.flags = r.readUI8();

        return f;

    }


    private Tag decodePlaceObject(int length) throws IOException
    {
        PlaceObject t = new PlaceObject(stagPlaceObject);
        int pos = r.getOffset();
        int idref = r.readUI16();
        t.depth = r.readUI16();
        t.setMatrix(decodeMatrix());
        if (length - r.getOffset() + pos != 0)
        {
            t.setCxform(decodeCxform());
        }
        t.setRef(dict.getTag(idref));
        return t;
    }

    private Tag decodePlaceObject2(int length) throws IOException
    {
        PlaceObject t;
        t = new PlaceObject(stagPlaceObject2);
        r.syncBits();

        int pos = r.getOffset();

        t.flags = r.readUI8();
        t.depth = r.readUI16();

        if (t.hasCharID())
        {
            int idref = r.readUI16();
            t.ref = dict.getTag(idref);
        }
        if (t.hasMatrix())
        {
            t.matrix = decodeMatrix();
        }
        if (t.hasCxform())
        {
            // ed 5/22/03 the SWF 6 file format spec says this will be a CXFORM, but
            // the spec is wrong.  the player expects a CXFORMA.
            t.colorTransform = decodeCxforma();
        }
        if (t.hasRatio())
        {
            t.ratio = r.readUI16();
        }
        if (t.hasName())
        {
            t.name = r.readString();
        }
        if (t.hasClipDepth())
        {
            t.clipDepth = r.readUI16();
        }
        if (t.hasClipAction())
        {
            ActionDecoder actionDecoder = new ActionDecoder(r,swd);
            actionDecoder.setKeepOffsets(keepOffsets);
            t.clipActions = actionDecoder.decodeClipActions(length - (r.getOffset() - pos));
        }
        return t;
    }


    private CXFormWithAlpha decodeCxforma() throws IOException
    {
        CXFormWithAlpha c = new CXFormWithAlpha();
        r.syncBits();
        c.hasAdd = r.readBit();
        c.hasMult = r.readBit();
        int nbits = r.readUBits(4);
        if (c.hasMult)
        {
            c.redMultTerm = r.readSBits(nbits);
            c.greenMultTerm = r.readSBits(nbits);
            c.blueMultTerm = r.readSBits(nbits);
            c.alphaMultTerm = r.readSBits(nbits);
        }
        if (c.hasAdd)
        {
            c.redAddTerm = r.readSBits(nbits);
            c.greenAddTerm = r.readSBits(nbits);
            c.blueAddTerm = r.readSBits(nbits);
            c.alphaAddTerm = r.readSBits(nbits);
        }
        return c;
    }

    private Tag decodeProtect(int length) throws IOException
    {
        GenericTag t;
        t = new GenericTag(stagProtect);
        t.data = new byte[length];
        r.readFully(t.data);
        return t;
    }

    private Tag decodeDefineButtonCxform() throws IOException
    {
        DefineButtonCxform t;
        t = new DefineButtonCxform();
        int idref = r.readUI16();
        t.button = (DefineButton) dict.getTag(idref);
        if (t.button.cxform != null)
        {
            handler.error("button " + dict.getId(t.button) + " cxform redefined");
        }
        t.button.cxform = t;
        t.colorTransform = decodeCxform();
        return t;
    }

    private Tag decodeDefineJPEG2(int length) throws IOException
    {
        DefineBits t = new DefineBits(stagDefineBitsJPEG2);
        int pos = r.getOffset();
        int id = r.readUI16();
        length -= r.getOffset() - pos;
        t.data = new byte[length];
        r.readFully(t.data);

        dict.add(id, t);
        return t;
    }

    private Tag decodeDefineBitsLossless(int length) throws IOException
    {
        DefineBitsLossless t = new DefineBitsLossless(stagDefineBitsLossless);
        SwfDecoder r1 = r;

        int pos = r1.getOffset();

        int id = r1.readUI16();
        t.format = r1.readUI8();
        t.width = r1.readUI16();
        t.height = r1.readUI16();

        byte[] data;

        switch (t.format)
        {
        case 3:
            int tableSize = r1.readUI8() + 1;
            length -= r1.getOffset() - pos;
            data = new byte[length];
            r1.readFully(data);
            r1 = new SwfDecoder(new InflaterInputStream(new ByteArrayInputStream(data)), getSwfVersion());
            decodeColorMapData(r1, t, tableSize);
            break;
        case 4:
        case 5:
            length -= r1.getOffset() - pos;
            data = new byte[length];
            r1.readFully(data);
            r1 = new SwfDecoder(new InflaterInputStream(new ByteArrayInputStream(data)), getSwfVersion());
            t.data = new byte[t.width * t.height * 4];
            r1.readFully(t.data);
            break;
        default:
            throw new SwfFormatException("Illegal bitmap format " + t.format);
        }

        dict.add(id, t);
        return t;
    }

    private void decodeColorMapData(SwfDecoder r1, DefineBitsLossless tag, int tableSize) throws IOException
    {
        tag.colorData = new int[tableSize];

        for (int i = 0; i < tableSize; i++)
        {
            tag.colorData[i] = decodeRGB(r1);
        }

        int width = tag.width;
        int height = tag.height;

        if (width % 4 != 0)
        {
            width = (width / 4 + 1) * 4;
        }

        tag.data = new byte[width * height];

        r1.readFully(tag.data);
    }

    private Tag decodeSoundStreamBlock(int length) throws IOException
    {
        GenericTag t = new GenericTag(stagSoundStreamBlock);
        t.data = new byte[length];
        r.readFully(t.data);
        return t;
    }

    private Tag decodeSoundStreamHead(int code) throws IOException
    {
        SoundStreamHead t;
        t = new SoundStreamHead(code);
        r.syncBits();

        // mixFormat
        r.readUBits(4); // reserved
        t.playbackRate = r.readUBits(2);
        t.playbackSize = r.readUBits(1);
        t.playbackType = r.readUBits(1);

        // format
        t.compression = r.readUBits(4);
        t.streamRate = r.readUBits(2);
        t.streamSize = r.readUBits(1);
        t.streamType = r.readUBits(1);

        t.streamSampleCount = r.readUI16();

		if (t.compression == SoundStreamHead.sndCompressMP3)
		{
			t.latencySeek = r.readSI16();
		}
        return t;
    }

    private Tag decodeDefineButtonSound() throws IOException
    {
        DefineButtonSound t;
        t = new DefineButtonSound();
        int idref = r.readUI16();
        t.button = (DefineButton) dict.getTag(idref);
        if (t.button.sounds != null)
        {
            handler.error("button " + idref + " sound redefined");
        }
        t.button.sounds = t;

        idref = r.readUI16();
        if (idref != 0)
        {
            t.sound0 = dict.getTag(idref);
            t.info0 = decodeSoundInfo();
        }
        idref = r.readUI16();
        if (idref != 0)
        {
            t.sound1 = dict.getTag(idref);
            t.info1 = decodeSoundInfo();
        }
        idref = r.readUI16();
        if (idref != 0)
        {
            t.sound2 = dict.getTag(idref);
            t.info2 = decodeSoundInfo();
        }
        idref = r.readUI16();
        if (idref != 0)
        {
            t.sound3 = dict.getTag(idref);
            t.info3 = decodeSoundInfo();
        }
        return t;
    }

    private Tag decodeStartSound() throws IOException
    {
        StartSound t;
        t = new StartSound();
        int idref = r.readUI16();
        t.sound = (DefineSound) dict.getTag(idref);
        t.soundInfo = decodeSoundInfo();
        return t;
    }

    private SoundInfo decodeSoundInfo() throws IOException
    {
        SoundInfo i = new SoundInfo();

        r.syncBits();

        r.readUBits(2); // reserved
        i.syncStop = r.readBit();
        i.syncNoMultiple = r.readBit();

        boolean hasEnvelope = r.readBit();
        boolean hasLoops = r.readBit();
        boolean hasOutPoint = r.readBit();
        boolean hasInPoint = r.readBit();

        if (hasInPoint)
        {
            i.inPoint = r.readUI32();
        }
        if (hasOutPoint)
        {
            i.outPoint = r.readUI32();
        }
        if (hasLoops)
        {
            i.loopCount = r.readUI16();
        }
        if (hasEnvelope)
        {
            int points = r.readUI8();
            i.records = new long[points];
            for (int k = 0; k < points; k++)
            {
                i.records[k] = r.read64();
            }
        }

        return i;
    }

    private Tag decodeDefineSound(int length) throws IOException
    {
        DefineSound t;
        t = new DefineSound();
        int pos = r.getOffset();

        int id = r.readUI16();

        r.syncBits();

        t.format = r.readUBits(4);
        t.rate = r.readUBits(2);
        t.size = r.readUBits(1);
        t.type = r.readUBits(1);
        t.sampleCount = r.readUI32();

        length -= r.getOffset() - pos;

        t.data = new byte[length];
        r.readFully(t.data);

        dict.add(id, t);
        return t;
    }

    private Tag decodeDefineFontInfo(int code, int length) throws IOException
    {
        DefineFontInfo t;
        t = new DefineFontInfo(code);
        int pos = r.getOffset();

        int idref = r.readUI16();
        t.font = (DefineFont1) dict.getTag(idref);
        if (t.font.fontInfo != null)
        {
            handler.error("font " + idref + " info redefined");
        }
        t.font.fontInfo = t;
        t.name = r.readLengthString();

        r.syncBits();

        r.readUBits(3); // reserved
        t.shiftJIS = r.readBit();
        t.ansi = r.readBit();
        t.italic = r.readBit();
        t.bold = r.readBit();
        t.wideCodes = r.readBit();

        if (code == stagDefineFontInfo2)
        {
            if (!t.wideCodes)
                handler.error("widecodes must be true in DefineFontInfo2");
            if (getSwfVersion() < 6)
                handler.error("DefineFont2 not valid before SWF6");
            t.langCode = r.readUI8();
        }

        length -= r.getOffset() - pos;

        if (t.wideCodes)
        {
            length = length / 2;
            t.codeTable = new char[length];
            for (int i = 0; i < length; i++)
            {
                t.codeTable[i] = (char)r.readUI16();
            }
        }
        else
        {
            t.codeTable = new char[length];
            for (int i = 0; i < length; i++)
            {
                t.codeTable[i] = (char)r.readUI8();
            }
        }
        return t;
    }

    private Tag decodeDoAction(int length) throws IOException
    {
        DoAction t = new DoAction();
        ActionDecoder actionDecoder = new ActionDecoder(r,swd);
        actionDecoder.setKeepOffsets(keepOffsets);
        t.actionList = actionDecoder.decode(length);
        return t;
    }

    private Tag decodeDefineText(int type) throws IOException
    {
        DefineText t = new DefineText(type);

        int id = r.readUI16();
        t.bounds = decodeRect();
        t.matrix = decodeMatrix();

        int glyphBits = r.readUI8();
        int advanceBits = r.readUI8();
        // todo range check - glyphBits and advanceBits must be <= 32
        ArrayList<TextRecord> list = new ArrayList<TextRecord>(2);

        int code;
        while ((code = r.readUI8()) != 0)
        {
            list.add(decodeTextRecord(type, code, glyphBits, advanceBits));
        }

        t.records = list;

        dict.add(id, t);
        return t;
    }

    private GlyphEntry[] decodeGlyphEntries(int glyphBits, int advanceBits, int count) throws IOException
    {
        GlyphEntry[] e = new GlyphEntry[count];

        r.syncBits();
        for (int i = 0; i < count; i++)
        {
            GlyphEntry ge = new GlyphEntry();

            ge.setIndex( r.readUBits(glyphBits) );
            ge.advance = r.readSBits(advanceBits);

            e[i] = ge;
        }

        return e;
    }

    private TextRecord decodeTextRecord(int defineText, int flags, int glyphBits, int advanceBits) throws IOException
    {
        TextRecord t = new TextRecord();

        t.flags = flags;

        if (t.hasFont())
        {
            int idref = r.readUI16();
            t.font = (DefineFont) dict.getTag(idref);
        }

        if (t.hasColor())
        {
            switch (defineText)
            {
            case stagDefineText:
                t.color = decodeRGB(r);
                break;
            case stagDefineText2:
                t.color = decodeRGBA(r);
                break;
            default:
                assert false;
            }
        }

        if (t.hasX())
        {
            t.xOffset = r.readSI16();
        }

        if (t.hasY())
        {
            t.yOffset = r.readSI16();
        }

        if (t.hasHeight())
        {
            t.height = r.readUI16();
        }

        int count = r.readUI8();
        t.entries = decodeGlyphEntries(glyphBits, advanceBits, count);

        return t;
    }

    private Tag decodeDefineFont() throws IOException
    {
        DefineFont1 t;
        t = new DefineFont1();
        int id = r.readUI16();
        int offset = r.readUI16();
        int numGlyphs = offset/2;

        t.glyphShapeTable = new Shape[numGlyphs];

        // skip the offset table
        for (int i = 1; i < numGlyphs; i++)
        {
            r.readUI16();
        }

        for (int i = 0; i < numGlyphs; i++)
        {
            t.glyphShapeTable[i] = decodeShape(stagDefineShape3);
        }

        dict.add(id, t);
        dict.addFontFace( t );
        return t;
    }

    private Tag decodeSetBackgroundColor() throws IOException
    {
        SetBackgroundColor t;
        t = new SetBackgroundColor( decodeRGB(r));
        return t;
    }

	/**
	 * decode jpeg tables.  only one per movie.  second and subsequent
	 * occurences of this tag are ignored by the player.
	 * @param length
	 * @return
	 * @throws IOException
	 */
    private GenericTag decodeJPEGTables(int length) throws IOException
    {
        GenericTag t;
        t = new GenericTag(stagJPEGTables);
        t.data = new byte[length];
        r.readFully(t.data);
        return t;
    }

    private Tag decodeDefineButton(int length) throws IOException
    {
		int startPos = r.getOffset();
        DefineButton t;
        t = new DefineButton(stagDefineButton);
        int id = r.readUI16();

        ArrayList<ButtonRecord> list = new ArrayList<ButtonRecord>();
        ButtonRecord record;
        do
        {
            record = decodeButtonRecord(t.code);
            if (record != null)
            {
                list.add(record);
            }
        }
        while (record != null);

        t.buttonRecords = new ButtonRecord[list.size()];
        list.toArray(t.buttonRecords);

        // old school button actions only handle one possible transition
        int consumed = r.getOffset()-startPos;
        t.condActions = new ButtonCondAction[1];
        t.condActions[0].overDownToOverUp = true;
        ActionDecoder actionDecoder = new ActionDecoder(r,swd);
        actionDecoder.setKeepOffsets(keepOffsets);
        t.condActions[0].actionList = actionDecoder.decode(length-consumed);
        t.trackAsMenu = false;

        dict.add(id, t);
        return t;
    }

    private ButtonRecord decodeButtonRecord(int type) throws IOException
    {
    	boolean hasFilterList = false, hasBlendMode = false;
        ButtonRecord b = new ButtonRecord();

        r.syncBits();

        int reserved;
        if (type == stagDefineButton2)
        {
            reserved = r.readUBits(2);
            hasBlendMode = r.readBit();
            hasFilterList = r.readBit();
        }
        else
        {
            reserved = r.readUBits(4);
        }
        b.hitTest = r.readBit();
        b.down = r.readBit();
        b.over = r.readBit();
        b.up = r.readBit();

        if (reserved == 0 && !b.hitTest && !b.down && !b.over && !b.up)
        {
            return null;
        }

        int idref = r.readUI16();
        b.characterRef = dict.getTag(idref);
        b.placeDepth = r.readUI16();
        b.placeMatrix = decodeMatrix();

        if (type == stagDefineButton2)
        {
            b.colorTransform = decodeCxforma();
            if (hasFilterList)
            {
            	b.filters = decodeFilterList();
            }
            if (hasBlendMode)
            {
            	b.blendMode = r.readUI8();
            }
        }

        return b;
    }

    private Tag decodeDefineBinaryData(int length) throws IOException
    {
        DefineBinaryData t = new DefineBinaryData();
        int pos = r.getOffset();
        int id = r.readUI16();
        t.reserved = r.readSI32();
        length -= r.getOffset() - pos;
        t.data = new byte[length];
        r.readFully(t.data);
        dict.add(id, t);
        return t;
    }

    private Tag decodeDefineBits(int length) throws IOException
    {
        DefineBits t;
        t = new DefineBits(stagDefineBits);
        int pos = r.getOffset();
        int id = r.readUI16();
        length -= r.getOffset() - pos;
        t.data = new byte[length];
        r.readFully(t.data);
        t.jpegTables = jpegTables;
        dict.add(id, t);
        return t;
    }

    private Tag decodeRemoveObject(int code) throws IOException
    {
        RemoveObject t;
        t = new RemoveObject(code);
        if (code == stagRemoveObject)
        {
            int idref = r.readUI16();
            t.ref = dict.getTag(idref);
        }
        t.depth = r.readUI16();
        return t;
    }

    private CXForm decodeCxform() throws IOException
    {
        CXForm c = new CXForm();
        r.syncBits();

        c.hasAdd = r.readBit();
        c.hasMult = r.readBit();
        int nbits = r.readUBits(4);
        if (c.hasMult)
        {
            c.redMultTerm = r.readSBits(nbits);
            c.greenMultTerm = r.readSBits(nbits);
            c.blueMultTerm = r.readSBits(nbits);
        }
        if (c.hasAdd)
        {
            c.redAddTerm = r.readSBits(nbits);
            c.greenAddTerm = r.readSBits(nbits);
            c.blueAddTerm = r.readSBits(nbits);
        }
        return c;
    }

    private Tag decodeMetadata() throws IOException
    {
        Metadata t = new Metadata();
        t.xml = r.readString();
        return t;
    }

    private Tag decodeDefineShape(int shape) throws IOException
    {
        DefineShape t = new DefineShape(shape);

        int id = r.readUI16();
        t.bounds = decodeRect();
        if (shape == stagDefineShape4)
        {
            t.edgeBounds = decodeRect();
            r.readUBits(5);
            t.usesFillWindingRule = r.readBit();
            t.usesNonScalingStrokes = r.readBit();
            t.usesScalingStrokes = r.readBit();
        }
        t.shapeWithStyle = decodeShapeWithStyle(shape);

        dict.add(id, t);
        return t;
    }

    private ShapeWithStyle decodeShapeWithStyle(int shape) throws IOException
    {
        ShapeWithStyle sw = new ShapeWithStyle();

        r.syncBits();

        sw.fillstyles = decodeFillstyles(shape);
        sw.linestyles = decodeLinestyles(shape);

        Shape s = decodeShape(shape);

        sw.shapeRecords = s.shapeRecords;

        return sw;
    }

    private ArrayList<LineStyle> decodeLinestyles(int shape) throws IOException
    {
        ArrayList<LineStyle> a = new ArrayList<LineStyle>();

        int count = r.readUI8();
        if (count == 0xFF)
        {
            count = r.readUI16();
        }

        for (int i = 0; i < count; i++)
        {
            a.add(decodeLineStyle(shape));
        }

        return a;
    }

    private LineStyle decodeLineStyle(int shape) throws IOException
    {
        LineStyle s = new LineStyle();
        s.width = r.readUI16();

        if (shape == stagDefineShape4)
        {
            s.flags = r.readUI16();
            if (s.hasMiterJoint())
                s.miterLimit = r.readUI16();    // 8.8 fixedpoint
        }
        if ((shape == stagDefineShape4) && (s.hasFillStyle()))
        {
            s.fillStyle = decodeFillStyle(shape);
        }
        else if ((shape == stagDefineShape3) || (shape == stagDefineShape4))
        {
            s.color = decodeRGBA(r);
        }
        else
        {
            s.color = decodeRGB(r);
        }

        return s;
    }

    private ArrayList<FillStyle> decodeFillstyles(int shape) throws IOException
    {
        ArrayList<FillStyle> a = new ArrayList<FillStyle>();

        int count = r.readUI8();
        if (count == 0xFF)
        {
            count = r.readUI16();
        }

        for (int i = 0; i < count; i++)
        {
            a.add(decodeFillStyle(shape));
        }

        return a;
    }

    private FillStyle decodeFillStyle(int shape) throws IOException
    {
        FillStyle s = new FillStyle();

        s.type = r.readUI8();
        switch (s.type)
        {
        case FillStyle.FILL_SOLID: // 0x00
            if (shape == stagDefineShape3 || shape == stagDefineShape4)
                s.color = decodeRGBA(r);
            else if (shape == stagDefineShape2 || shape == stagDefineShape)
                s.color = decodeRGB(r);
            else
                throw new SwfFormatException("bad shape code");
            break;
        case FillStyle.FILL_GRADIENT: // 0x10 linear gradient fill
        case FillStyle.FILL_RADIAL_GRADIENT: // 0x12 radial gradient fill
        case FillStyle.FILL_FOCAL_RADIAL_GRADIENT: // 0x13 focal radial gradient fill
            s.matrix = decodeMatrix();
            s.gradient = decodeGradient(shape, s.type);
            break;
        case FillStyle.FILL_BITS: // 0x40 tiled bitmap fill
        case (FillStyle.FILL_BITS | FillStyle.FILL_BITS_CLIP): // 0x41 clipped bitmap fill
        case (FillStyle.FILL_BITS | FillStyle.FILL_BITS_NOSMOOTH): // 0x42 tiled non-smoothed fill
        case (FillStyle.FILL_BITS | FillStyle.FILL_BITS_CLIP | FillStyle.FILL_BITS_NOSMOOTH): // 0x43 clipped non-smoothed fill
            int idref = r.readUI16();
            try
            {
                s.bitmap = dict.getTag(idref);
            }
            catch (IllegalArgumentException e)
            {
                s.bitmap = null;
                handler.error(e.getMessage());
            }
            s.matrix = decodeMatrix();
            break;
        default:
            throw new SwfFormatException("unrecognized fill style type: " + s.type);
        }

        return s;
    }

    private Gradient decodeGradient(int shape, int filltype) throws IOException
    {
        Gradient gradient = (filltype == FillStyle.FILL_FOCAL_RADIAL_GRADIENT)? new FocalGradient() : new Gradient();
        r.syncBits();
        gradient.spreadMode = r.readUBits( 2 );
        gradient.interpolationMode = r.readUBits( 2 );
        int count = r.readUBits( 4 );
        gradient.records = new GradRecord[count];

        for (int i = 0; i < count; i++)
        {
            gradient.records[i] = decodeGradRecord(shape);
        }

        if (filltype == FillStyle.FILL_FOCAL_RADIAL_GRADIENT)
        {
            ((FocalGradient)gradient).focalPoint = r.readFixed8();
        }

        return gradient;
    }

    private GradRecord decodeGradRecord(int shape) throws IOException
    {
        GradRecord g = new GradRecord();
        g.ratio = r.readUI8();

        switch (shape)
        {
        case stagDefineShape:
        case stagDefineShape2:
            g.color = decodeRGB(r);
            break;
        case stagDefineShape3:
        case stagDefineShape4:
            g.color = decodeRGBA(r);
            break;
        }

        return g;
    }

    private Matrix decodeMatrix() throws IOException
    {
        Matrix m = new Matrix();

        r.syncBits();
        m.hasScale = r.readBit();
        if (m.hasScale)
        {
            int nScaleBits = r.readUBits(5);
            m.scaleX = r.readSBits(nScaleBits);
            m.scaleY = r.readSBits(nScaleBits);
        }

        m.hasRotate = r.readBit();
        if (m.hasRotate)
        {
            int nRotateBits = r.readUBits(5);
            m.rotateSkew0 = r.readSBits(nRotateBits);
            m.rotateSkew1 = r.readSBits(nRotateBits);
        }

        int nTranslateBits = r.readUBits(5);
        m.translateX = r.readSBits(nTranslateBits);
        m.translateY = r.readSBits(nTranslateBits);

        return m;
    }

    private int decodeRGBA(SwfDecoder r) throws IOException
    {
        int color = r.readUI8() << 16; // red
        color |= r.readUI8() << 8; // green
        color |= r.readUI8(); // blue
        color |= r.readUI8() << 24; // alpha

        // resulting format is 0xAARRGGBB
        return color;
    }

    private int decodeRGB(SwfDecoder r) throws IOException
    {
        int color = r.readUI8() << 16; // red
        color |= r.readUI8()<<8; // green
        color |= r.readUI8(); // blue

        // resulting format is 0x00RRGGBB
        return color;
    }

    private Shape decodeGlyph(int shape, int count) throws IOException
    {
        Shape s1 = new Shape();

        r.syncBits();

        // SDK-18153 - Hack to work around third-party generated SWFs that
        // do not include at least one shape record in glyph SHAPE.
        if (count > 0)
        {
            // we use int[1] so we can pass numBits by reference
            int[] numFillBits = new int[] { r.readUBits(4) };
            int[] numLineBits = new int[] { r.readUBits(4) };

            if (count > 1)
            {
                s1.shapeRecords = decodeShapeRecords(shape, numFillBits, numLineBits);
            }
        }

        return s1;
    }

    private Shape decodeShape(int shape) throws IOException
    {
        Shape s1 = new Shape();

        r.syncBits();

        // we use int[1] so we can pass numBits by reference
        int[] numFillBits = new int[] { r.readUBits(4) };
        int[] numLineBits = new int[] { r.readUBits(4) };

        s1.shapeRecords = decodeShapeRecords(shape, numFillBits, numLineBits);

        return s1;
    }

    private List<ShapeRecord> decodeShapeRecords(int shape, int[] numFillBits, int[] numLineBits) throws IOException
    {
        ArrayList<ShapeRecord> list = new ArrayList<ShapeRecord>();
        boolean endShapeRecord = false;
        do
        {
            if (r.readBit())
            {
                // edge
                if (r.readBit())
                {
                    // line
                    list.add(decodeStraightEdgeRecord());
                }
                else
                {
                    // curve
                    list.add(decodeCurvedEdgeRecord());
                }
            }
            else
            {
                // style change
                boolean stateNewStyles = r.readBit();
                boolean stateLineStyle = r.readBit();
                boolean stateFillStyle1 = r.readBit();
                boolean stateFillStyle0 = r.readBit();
                boolean stateMoveTo = r.readBit();

                if (stateNewStyles || stateLineStyle || stateFillStyle1 ||
                        stateFillStyle0 || stateMoveTo)
                {
                    StyleChangeRecord s = decodeStyleChangeRecord(stateNewStyles, stateLineStyle,
                                                                  stateFillStyle1, stateFillStyle0, stateMoveTo,
                                                                  shape, numFillBits, numLineBits);

                    list.add(s);
                }
                else
                {
                    endShapeRecord = true;
                }
            }
        }
        while (!endShapeRecord);

        return list;
    }

    private CurvedEdgeRecord decodeCurvedEdgeRecord() throws IOException
    {
        CurvedEdgeRecord s = new CurvedEdgeRecord();
        int nbits = 2+r.readUBits(4);
        s.controlDeltaX = r.readSBits(nbits);
        s.controlDeltaY = r.readSBits(nbits);
        s.anchorDeltaX = r.readSBits(nbits);
        s.anchorDeltaY = r.readSBits(nbits);
        return s;
    }

    private StraightEdgeRecord decodeStraightEdgeRecord() throws IOException
    {
        int nbits = 2+r.readUBits(4);
        if (r.readBit())
        {
            // general line
            int dx = r.readSBits(nbits);
            int dy = r.readSBits(nbits);
            return new StraightEdgeRecord(dx, dy);
        }
        else
        {
            if (r.readBit())
            {
                // vertical
                int dy = r.readSBits(nbits);
                return new StraightEdgeRecord(0, dy);
            }
            else
            {
                // horizontal
                int dx = r.readSBits(nbits);
                return new StraightEdgeRecord(dx, 0);
            }
        }
    }

    private StyleChangeRecord decodeStyleChangeRecord(boolean stateNewStyles,
                                                      boolean stateLineStyle,
                                                      boolean stateFillStyle1,
                                                      boolean stateFillStyle0,
                                                      boolean stateMoveTo,
                                                      int shape,
                                                      int[] numFillBits,
                                                      int[] numLineBits) throws IOException
    {
        StyleChangeRecord s = new StyleChangeRecord();

        s.stateNewStyles = stateNewStyles;
        s.stateLineStyle = stateLineStyle;
        s.stateFillStyle1 = stateFillStyle1;
        s.stateFillStyle0 = stateFillStyle0;
        s.stateMoveTo = stateMoveTo;

        if (s.stateMoveTo)
		{
			int moveBits = r.readUBits(5);
			s.moveDeltaX = r.readSBits(moveBits);
			s.moveDeltaY = r.readSBits(moveBits);
		}

        if (s.stateFillStyle0)
        {
            s.fillstyle0 = r.readUBits(numFillBits[0]);
        }

        if (s.stateFillStyle1)
        {
            s.fillstyle1 = r.readUBits(numFillBits[0]);
        }

        if (s.stateLineStyle)
        {
            s.linestyle = r.readUBits(numLineBits[0]);
        }

        if (s.stateNewStyles)
        {
            s.fillstyles = decodeFillstyles(shape);
            s.linestyles = decodeLinestyles(shape);

            r.syncBits();

            numFillBits[0] = r.readUBits(4);
            numLineBits[0] = r.readUBits(4);
        }
        return s;
    }

    private Tag decodeDefineSprite(int endpos) throws IOException
    {
        DefineSprite t = new DefineSprite();
        t.header = header;
        int id = r.readUI16();
        t.framecount = r.readUI16();
        decodeTags(t.tagList);
        while (r.getOffset() < endpos)
        {
            // extra data at end of sprite.  must be zero
            int b = r.readUI8();
            if (b != 0)
            {
                throw new SwfFormatException("nonzero data past end of sprite");
            }
        }
        dict.add(id, t);
        return t;
    }

    public Tag decodeSerialNumber() throws IOException
    {
        int product = r.readSI32();
        int edition = r.readSI32();

        byte[] version = new byte[2];
        r.read(version);
        byte majorVersion = version[0];
        byte minorVersion = version[1];

        long build = r.read64();
        long compileDate = r.read64();

        return new ProductInfo(product, edition, majorVersion, minorVersion, build, compileDate);
    }

    public Header decodeHeader() throws IOException, FatalParseException
    {
        Header header = new Header();
        byte[] sig = new byte[8];

        new DataInputStream(swfIn).readFully(sig);
        header.version = sig[3];
        header.length = sig[4]&0xFF | (sig[5]&0xFF)<<8 | (sig[6]&0xFF)<<16 | sig[7]<<24;

        if (sig[0] == 'C' && sig[1] == 'W' && sig[2] == 'S')
        {
            header.compressed = true;
            r = new SwfDecoder(new InflaterInputStream(swfIn), header.version, 8);
        }
        else if (sig[0] == 'F' || sig[1] == 'W' || sig[2] == 'S')
        {
            r = new SwfDecoder(swfIn, header.version, 8);
        }
        else
        {
            handler.error("Invalid signature found.  Not a SWF file");
            throw new FatalParseException();
        }

        header.size = decodeRect();
        header.rate = r.readUI8() << 8 | r.readUI8();
        header.framecount = r.readUI16();

        return header;
    }

    public Tag decodeFileAttributes() throws IOException
    {
        FileAttributes tag = new FileAttributes();
        r.syncBits();
        
        r.readUBits(1); //reserved
        tag.useDirectBlit = r.readBit();
        tag.useGPU = r.readBit();
        tag.hasMetadata = r.readBit();
        
        tag.actionScript3 = r.readBit();
        tag.suppressCrossDomainCaching = r.readBit();
        tag.swfRelativeUrls = r.readBit();
        tag.useNetwork = r.readBit();
        
        tag.brokerProductManager = r.readBit();
        tag.brokerLocalConnection = r.readBit();
        r.readUBits(22); //reserved
        return tag;
    }
    
    public Tag decodeEnableTelemetry() throws IOException
    {
    	EnableTelemetry tag = new EnableTelemetry();
        r.syncBits();
        r.readUBits(16); //reserved
        tag.enabled = true;
        return tag;
    }

    public Tag decodeDefineFontAlignZones() throws IOException
    {
        DefineFontAlignZones zones = new DefineFontAlignZones();
        int fontID = r.readUI16();
        zones.font = (DefineFont3)dict.getTag(fontID);
        zones.font.zones = zones;
        zones.csmTableHint = r.readUBits(2);
        r.readUBits(6);  // reserved
        zones.zoneTable = new ZoneRecord[zones.font.glyphShapeTable.length];
        for (int i = 0; i < zones.font.glyphShapeTable.length; i++)
        {
            ZoneRecord record = new ZoneRecord();
            zones.zoneTable[i] = record;
            record.numZoneData = r.readUI8();
            record.zoneData = new long[record.numZoneData];
            for (int j = 0; j < record.numZoneData; j++)
            {
                record.zoneData[j] = r.readUI32();
            }
            record.zoneMask = r.readUI8();
        }
        return zones;
    }

    public Tag decodeCSMTextSettings() throws IOException
    {
        CSMTextSettings tag = new CSMTextSettings();
        int textID = r.readUI16();
        if (textID != 0)
        {
            tag.textReference = dict.getTag(textID);
            if (tag.textReference instanceof DefineText)
            {
                ((DefineText)tag.textReference).csmTextSettings = tag;
            }
            else if (tag.textReference instanceof DefineEditText)
            {
                ((DefineEditText)tag.textReference).csmTextSettings = tag;
            }
            else
            {
                handler.error("CSMTextSettings' textID must reference a valid DefineText or DefineEditText.  References " + tag.textReference);
            }
        }
        tag.styleFlagsUseSaffron = r.readUBits(2);
        tag.gridFitType = r.readUBits(3);
        r.readUBits(3); // reserved
        // FIXME: thickness/sharpness should be read in as 32 bit IEEE Single Precision format in little Endian
        tag.thickness = r.readUBits(32);
        tag.sharpness = r.readUBits(32);
        r.readUBits(8); // reserved
        return tag;
    }

	public Tag decodeDefineFontName() throws IOException
	{
	    DefineFontName tag = new DefineFontName();
	    int fontID = r.readUI16();
	    tag.font = (DefineFont)dict.getTag(fontID);
	    tag.font.license = tag;
		tag.fontName = r.readString();
		tag.copyright = r.readString();
		return tag;
	}

    private Rect decodeRect() throws IOException
    {
        r.syncBits();

        Rect rect = new Rect();

        int nBits = r.readUBits(5);
        rect.xMin = r.readSBits(nBits);
        rect.xMax = r.readSBits(nBits);
        rect.yMin = r.readSBits(nBits);
        rect.yMax = r.readSBits(nBits);

        return rect;
    }
}
