/*
 *
 *  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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.UTFDataFormatException;

/**
 * A decoder for a whole SWF.
 */
public final class SwfDecoder extends BufferedInputStream
{
	private int offset;
	private int bitBuf;
	private int bitPos;
    int swfVersion;

    /**
     * create a decoder that reads directly from this byte array
     * @param b
     * @param swfVersion
     */
    public SwfDecoder(byte[] b, int swfVersion)
    {
        this((InputStream)null, swfVersion);
        buf = b;
        count = b.length;
        pos = 0;
    }

    /**
     * create a buffering decoder that reads from this unbuffered
     * input stream.  Since SwfDecoder is a BufferedInputStream,
     * it is not necessary to provide a BufferedInputStream for good
     * performance.
     * @param in
     * @param swfVersion
     */
    public SwfDecoder(InputStream in, int swfVersion)
	{
        super(in);
        this.swfVersion = swfVersion;
	}

    public SwfDecoder(InputStream in, int swfVersion, int offset)
    {
        this(in, swfVersion);
        this.offset = offset;
    }

	public void readFully(byte[] b) throws IOException
	{
		int remain = b.length;
		int off = 0;
		int count;
		while (remain > 0)
		{
			count = read(b, off, remain);
			if (count > 0)
			{
				off += count;
				remain -= count;
			}
			else
			{
				throw new SwfFormatException("couldn't read " + remain);
			}
		}
	}

    public int read() throws IOException
    {
        offset++;
        return super.read();
    }

    public int read(byte b[], int off, int len)
            throws IOException
    {
        int n = super.read(b,off,len);
        offset += n;
        return n;
    }

    public synchronized long skip(long len) throws IOException
    {
        long n = super.skip(len);
        offset += n;
        return n;
    }

    public float readFixed8() throws IOException
    {
        int val = readUI16();
        // FIXME: this doesn't consider sign of original 8.8 value
        return (float)(val / 256.0);
    }

    public int readUI8() throws IOException
	{
        if (pos<count)
        {
            offset++;
            return buf[pos++]&0xFF;
        }
        else if (in != null)
        {
            offset++;
            return super.read();
        }
        else
        {
            return -1;
        }
	}

	public int readUI16() throws IOException
	{
        syncBits();
        int i;
        if (count-pos >= 2)
        {
            i = buf[pos] & 0xFF | (buf[pos + 1] & 0xFF) << 8;
            pos += 2;
            offset += 2;
        }
        else if (in != null)
        {                                 
            i = super.read() | super.read()<<8;
            offset += 2;
        }
        else
        {
            return -1;
        }
        return i;
	}

	public long readUI32() throws IOException
	{
        long i = readSI32() & 0xFFFFFFFFL;
        return i;
	}

    public int readSI32() throws IOException
    {
        syncBits();
        int i;
        if (count - pos >= 4)
        {
            i = buf[pos] & 0xFF | (buf[pos + 1] & 0xFF) << 8 | (buf[pos + 2] & 0xFF) << 16 | buf[pos + 3] << 24;
            offset += 4;
            pos += 4;
        }
        else if (in != null)
        {
            i = super.read() | super.read() << 8 | super.read() << 16 | super.read() << 24;
            offset += 4;
        }
        else
        {
            i = -1;
        }
        return i;
    }

    public long read64() throws IOException
    {
        return (readUI32() & 0xFFFFFFFFL) | (readUI32() << 32);
    }

	public boolean readBit() throws IOException
	{
		return readUBits(1) != 0;
	}

	public int readUBits(int numBits) throws IOException
	{
		if (numBits == 0)
		{
			return 0;
		}

		int bitsLeft = numBits;
		int result = 0;

		if (bitPos == 0) //no value in the buffer - read a byte
		{
			bitBuf = readUI8();
			bitPos = 8;
		}

		while (true)
		{
			int shift = bitsLeft - bitPos;
			if (shift > 0)
			{
				// Consume the entire buffer
				result |= bitBuf << shift;
				bitsLeft -= bitPos;

				// Get the next byte from the input stream
				bitBuf = readUI8();
				bitPos = 8;
			}
			else
			{
				// Consume a portion of the buffer
				result |= bitBuf >> -shift;
				bitPos -= bitsLeft;
				bitBuf &= 0xff >> (8 - bitPos);	// mask off the consumed bits

//                if (print) System.out.println("  read"+numBits+" " + result);
				return result;
			}
		}
	}

	public int readSBits(int numBits) throws IOException
	{
		if (numBits > 32)
		{
			throw new SwfFormatException("Number of bits > 32");
		}

		int num = readUBits(numBits);
        int shift = 32-numBits;
        // sign extension
        num = (num << shift) >> shift;
		return num;
	}

	public int readSI16() throws IOException
	{
		return (short)readUI16();
	}

    public float readFloat() throws IOException
    {
        int bits = readSI32();
        return Float.intBitsToFloat( bits );  
    }

    private final ByteArrayOutputStream out = new ByteArrayOutputStream(256)
    {
        public byte[] toByteArray()
        {
            // don't bother copying the array
            return buf;
        }
    };

	public String readLengthString() throws IOException
	{
        int length = readUI8();
        byte[] b = new byte[length];
        readFully(b);

        // [paul] Flash Authoring and the player null terminate the
        // string, so ignore the last byte when constructing the String.
        if (swfVersion >= 6)
        {
            return new String(b, 0, length - 1, "UTF8").intern();
        }
        else
        {
            // use platform encoding
            return new String(b, 0, length - 1).intern();
        }
	}

	public String readString() throws IOException
	{
        if (swfVersion >= 6)
        {
		    return readUTF().intern();
        }
        else
        {
            int ch;
            while ((ch = readUI8()) > 0)
            {
                out.write(ch);
            }
            // use platform encoding
            String s = new String(out.toByteArray(), 0, out.size());
            out.reset();
            return s.intern();
        }
	}

    private String readUTF() throws IOException
    {
        StringBuilder b = new StringBuilder();
        int c, c2, c3;

        while ((c = readUI8()) > 0)
        {
            switch (c >> 4)
            {
            case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                /* 0xxxxxxx*/
                b.append((char) c);
                break;

            case 12: case 13:
                /* 110x xxxx   10xx xxxx*/
                c2 = readUI8();
                if (c2 <= 0 || (c2 & 0xC0) != 0x80)
                    throw new UTFDataFormatException();
                b.append((char) ((c & 0x1F) << 6 | c2 & 0x3F));
                break;

            case 14:
                /* 1110 xxxx  10xx xxxx  10xx xxxx */
                c2 = readUI8();
                c3 = readUI8();
                if (c2 <= 0 || c3 <= 0 || ((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80))
                    throw new UTFDataFormatException();
                b.append((char) ((c & 0x0F) << 12 | (c2 & 0x3F) << 6 | c3 & 0x3F));
                break;

            default:
                /* 10xx xxxx,  1111 xxxx */
                throw new UTFDataFormatException();
            }
        }
        return b.toString();
    }


	public void syncBits()
	{
		bitPos = 0;
	}

	public int getOffset()
	{
		return offset;
	}
	
	private int markOffset;
	
	public void mark(int readlimit)
	{
		markOffset = offset;
		super.mark(readlimit);
	}
	
	public void reset() throws IOException
	{
		offset = markOffset;
		super.reset();
	}
	
}
