/*
* Copyright (c) 2003 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description:
*
*/


package javax.microedition.m3g;

import java.io.*;
import java.io.IOException;

import java.util.Vector;
import java.util.Hashtable;

import javax.microedition.io.*;
import javax.microedition.lcdui.Image;
import javax.microedition.io.Connector;
import javax.microedition.io.HttpConnection;
//#ifdef RD_JAVA_OMJ
import com.nokia.mj.impl.rt.support.Finalizer;
//#endif // RD_JAVA_OMJ

public class Loader
{
    // M3G
    static final byte[] M3G_FILE_IDENTIFIER =
    {
        -85, 74, 83, 82, 49, 56, 52, -69, 13, 10, 26, 10
    };
    // PNG
    static final byte[] PNG_FILE_IDENTIFIER =
    {
        -119, 80, 78, 71, 13, 10, 26, 10
    };
    static final int PNG_IHDR = ((73 << 24) + (72 << 16) + (68 << 8) + 82);
    static final int PNG_tRNS = ((116 << 24) + (82 << 16) + (78 << 8) + 83);
    static final int PNG_IDAT = ((73 << 24) + (68 << 16) + (65 << 8) + 84);

    // JPEG
    static final byte[] JPEG_FILE_IDENTIFIER =
    {
        -1, -40
    };
    static final int JPEG_JFIF = ((74 << 24) + (70 << 16) + (73 << 8) + 70);
    // Bytes before colour info in a frame header 'SOFn':
    // length (2 bytes), precision (1 byte), image height & width (4 bytes)
    static final int JPEG_SOFn_DELTA = 7;
    static final int JPEG_INVALID_COLOUR_FORMAT = -1;

    // File identifier types
    private static final int INVALID_HEADER_TYPE = -1;
    private static final int M3G_TYPE = 0;
    private static final int PNG_TYPE = 1;
    private static final int JPEG_TYPE = 2;

    // Misc.
    private static final int MAX_IDENTIFIER_LENGTH  = M3G_FILE_IDENTIFIER.length;

    // Initial buffer length for the header
    private static final int AVG_HEADER_SEC_LENGTH  = 64;

    // Initial buffer length for the xref section
    private static final int AVG_XREF_SEC_LENGTH    = 128;

    // Instance specific
    int handle;

    private Vector iLoadedObjects = new Vector();
    private Vector iFileHistory = new Vector();
    private String iResourceName = null;
    private String iParentResourceName = null;

    private int iTotalFileSize = 0;
    private int iBytesRead = M3G_FILE_IDENTIFIER.length;

    private byte[] iStreamData = null;
    private int iStreamOffset = 0;

    private Interface iInterface;

//#ifdef RD_JAVA_OMJ
    private Finalizer mFinalizer = new Finalizer()
    {
        public void finalizeImpl()
        {
            doFinalize();
        }
    };
//#endif // RD_JAVA_OMJ

    /**
     * Default ctor
     */
    private Loader()
    {
        iInterface = Interface.getInstance();
    }

    /**
     * Ctor
     * @param aFileHistory File storage
     * @param aParentResourceName Resource name
     */
    private Loader(Vector aFileHistory, String aParentResourceName)
    {
        iParentResourceName = aParentResourceName;
        iFileHistory        = aFileHistory;
        iInterface          = Interface.getInstance();
    }

    public static Object3D[] load(String name) throws IOException
    {
        if (name == null)
        {
            throw new NullPointerException();
        }

        try
        {
            return (new Loader()).loadFromStream(name);
        }
        catch (SecurityException e)
        {
            throw e;
        }
        catch (IOException e)
        {
            throw e;
        }
        catch (Exception e)
        {
            throw new IOException("Load error " + e);
        }
    }

    public static Object3D[] load(byte[] data, int offset) throws IOException
    {
        if (data == null)
        {
            throw new NullPointerException();
        }

        if (offset < 0 || offset >= data.length)
        {
            throw new IndexOutOfBoundsException();
        }
        try
        {
            return (new Loader()).loadFromByteArray(data, offset);
        }
        catch (SecurityException e)
        {
            throw e;
        }
        catch (IOException e)
        {
            throw e;
        }
        catch (Exception e)
        {
            throw new IOException("Load error " + e);
        }
    }

    /**
     * @see javax.microedition.m3g.Loader#load(String)
     */
    private Object3D[] loadFromStream(String aName) throws IOException
    {
        if (aName == null)
        {
            throw new NullPointerException();
        }

        if (inFileHistory(aName))
        {
            throw new IOException("Reference loop detected.");
        }
        iResourceName = aName;
        iFileHistory.addElement(aName);
        PeekInputStream stream = new PeekInputStream(
            getInputStream(aName), MAX_IDENTIFIER_LENGTH);
        // png, jpeg or m3g
        int type = getIdentifierType(stream);
        stream.rewind();
        iStreamData = null;
        iStreamOffset = 0;

        Object3D[] objects;
        try
        {
            objects = doLoad(stream, type);
        }
        finally
        {
            try
            {
                stream.close();
                stream = null;
            }
            catch (Exception e) {}
        }
        // Finally, remove file from history
        iFileHistory.removeElement(aName);
        return objects;
    }

    /**
     * @see javax.microedition.m3g.Loader#load(byte[], int)
     */
    private Object3D[] loadFromByteArray(byte[] aData, int aOffset) throws IOException
    {
        if (aData == null)
        {
            throw new NullPointerException("Resource byte array is null.");
        }
        int type = getIdentifierType(aData, aOffset);
        ByteArrayInputStream stream =
            new ByteArrayInputStream(aData, aOffset, aData.length - aOffset);
        iStreamData = aData;
        iStreamOffset = aOffset;
        iResourceName = "ByteArray";

        Object3D[] objects;
        try
        {
            objects = doLoad(stream, type);
        }
        finally
        {
            try
            {
                stream.close();
                stream = null;
            }
            catch (Exception e) {}
        }
        return objects;
    }

    /**
     * Dispatcher
     * @param aStream Source stream
     * @param aType Resource type
     */
    private Object3D[] doLoad(InputStream aStream, int aType) throws IOException
    {
        if (aType == M3G_TYPE)
        {
            return loadM3G(aStream);
        }
        else if (aType == PNG_TYPE)
        {
            return loadPNG(aStream);
        }
        else if (aType == JPEG_TYPE)
        {
            return loadJPEG(aStream);
        }
        throw new IOException("File not recognized.");
    }

    /**
     * PNG resource loader
     * @param aStream Resource stream
     * @return An array of newly created Object3D instances
     */
    private Object3D[] loadPNG(InputStream aStream) throws IOException
    {
        int format = Image2D.RGB;
        DataInputStream in = new DataInputStream(aStream);

        // Scan chuncs that have effect on Image2D format
        in.skip(PNG_FILE_IDENTIFIER.length);

        try
        {
            while (true)
            {
                int length = in.readInt();
                int type = in.readInt();
                // IHDR
                if (type == PNG_IHDR)
                {
                    in.skip(9);
                    int colourType = in.readUnsignedByte();
                    length -= 10;

                    switch (colourType)
                    {
                    case 0:
                        format = Image2D.LUMINANCE;
                        break;
                    case 2:
                        format = Image2D.RGB;
                        break;
                    case 3:
                        format = Image2D.RGB;
                        break;
                    case 4:
                        format = Image2D.LUMINANCE_ALPHA;
                        break;
                    case 6:
                        format = Image2D.RGBA;
                        break;
                    }
                }
                // tRNS
                if (type == PNG_tRNS)
                {
                    switch (format)
                    {
                    case Image2D.LUMINANCE:
                        format = Image2D.LUMINANCE_ALPHA;
                        break;
                    case Image2D.RGB:
                        format = Image2D.RGBA;
                        break;
                    }
                }
                // IDAT
                if (type == PNG_IDAT)
                {
                    break;
                }

                in.skip(length + 4);
            }
        }
        // EOF
        catch (Exception e)
        {
        }
        // Close the data stream
        try
        {
            in.close();
            in = null;
        }
        catch (Exception e) {}
        return buildImage2D(format);
    }

    /**
     * JPEG (with the same detailed definitions about the JPEG image format as defined in the
     * JSR 118 MIDP 2.1 specification for LCDUI) MUST be supported by compliant
     * implementations as a 2D bitmap image format for the Image2D class using the
     * javax.microedition.m3g.Loader class, and for M3G content files referencing bitmap images.
     * For colour JPEG images, the pixel format of the returned Image2D object MUST be
     * Image2D.RGB and for monochrome JPEG images, the pixel format MUST be
     * Image2D.LUMINANCE.
     *
     * JPEG marker: A two-byte code in which the first byte is 0xFF and the second
     * byte is a value between 1 and 0xFE.
     *
     * A JFIF file uses APP0 (0xe0) marker segments and constrains certain parameters in the frame.
     *
     * A frame header:
     * - 0xff, 'SOFn'
     * - length (2 bytes, Hi-Lo)
     * - data precision (1 byte)
     * - image height (2 bytes, Hi-Lo)
     * - image width (2 bytes, Hi-Lo)
     * - number of components (1 byte): 1 = grey scaled, 3 = color YCbCr or YIQ, 4 = color CMYK)
     *
     * @param aStream Resource stream
     * @return An array of newly created Object3D instances
     */
    private Object3D[] loadJPEG(InputStream aStream) throws IOException
    {
        int format = JPEG_INVALID_COLOUR_FORMAT;
        DataInputStream in = new DataInputStream(aStream);
        // Skip file identifier
        in.skip(JPEG_FILE_IDENTIFIER.length);
        try
        {
            int marker;
            do
            {
                // Find marker
                while (in.readUnsignedByte() != 0xff);
                do
                {
                    marker = in.readUnsignedByte();
                }
                while (marker == 0xff);

                // Parse marker
                switch (marker)
                {
                    // 'SOFn' (Start Of Frame n)
                case 0xC0:
                case 0xC1:
                case 0xC2:
                case 0xC3:
                case 0xC5:
                case 0xC6:
                case 0xC7:
                case 0xC9:
                case 0xCA:
                case 0xCB:
                case 0xCD:
                case 0xCE:
                case 0xCF:
                    // Skip length(2), precicion(1), width(2), height(2)
                    in.skip(JPEG_SOFn_DELTA);
                    switch (in.readUnsignedByte())
                    {
                    case 1:
                        format = Image2D.LUMINANCE;
                        break;
                    case 3:
                        format = Image2D.RGB;
                        break;
                    default:
                        throw new IOException("Unknown JPG format.");
                    }
                    break;
                    // APP0 (0xe0) marker segments and constrains certain parameters in the frame.
                case 0xe0:
                    int length = in.readUnsignedShort();
                    if (JPEG_JFIF != in.readInt())
                    {
                        throw new IOException("Not a valid JPG file.");
                    }
                    in.skip(length - 4 - 2);
                    break;
                default:
                    // Skip variable data
                    in.skip(in.readUnsignedShort() - 2);
                    break;
                }
            }
            while (format == JPEG_INVALID_COLOUR_FORMAT);
        }
        catch (Exception e) {}
        // Close the data stream
        try
        {
            in.close();
            in = null;
        }
        catch (Exception e) {}
        return buildImage2D(format);
    }

    /**
     * Image2D builder
     * @param aColourFormat Colour format
     * @return An array of newly created Object3D instances
     */
    private Object3D[] buildImage2D(int aColourFormat) throws IOException
    {
        InputStream stream;
        if (iStreamData == null)
        {
            stream = getInputStream(iResourceName);
        }
        else
        {
            stream = (InputStream) new ByteArrayInputStream(
                         iStreamData, iStreamOffset, iStreamData.length - iStreamOffset);
        }
        // Create an image object
        Image2D i2d;
        try
        {
            i2d = new Image2D(aColourFormat, Image.createImage(stream));
        }
        finally
        {
            try
            {
                stream.close();
            }
            catch (Exception e) {}
        }
        return new Object3D[] { i2d };
    }


    /**
     * M3G resource loader
     * @param aStream Resource stream
     * @return An array of newly created Object3D instances
     */
    private Object3D[] loadM3G(InputStream aStream) throws IOException
    {
        aStream.skip(M3G_FILE_IDENTIFIER.length);
        if (aStream instanceof PeekInputStream)
            ((PeekInputStream)aStream).increasePeekBuffer(AVG_HEADER_SEC_LENGTH);

        // Read header
        int compressionScheme = readByte(aStream);
        int totalSectionLength = readUInt32(aStream);
        if (aStream instanceof PeekInputStream && totalSectionLength > AVG_HEADER_SEC_LENGTH)
            ((PeekInputStream)aStream).increasePeekBuffer(totalSectionLength - AVG_HEADER_SEC_LENGTH);
        int uncompressedLength = readUInt32(aStream);

        int objectType = readByte(aStream);
        int length    = readUInt32(aStream);

        byte vMajor = (byte) readByte(aStream);
        byte vMinor = (byte) readByte(aStream);
        boolean externalLinks = readBoolean(aStream);
        iTotalFileSize = readUInt32(aStream);
        int approximateContentSize = readUInt32(aStream);
        String authoringField = readString(aStream);

        int checksum = readUInt32(aStream);

        /* Create and register a new native Loader */
        handle = _ctor(Interface.getHandle());
        Interface.register(this);

        if (externalLinks)
        {
            if (aStream instanceof PeekInputStream)
                ((PeekInputStream)aStream).increasePeekBuffer(AVG_XREF_SEC_LENGTH);
            loadExternalRefs(aStream);
            if (iLoadedObjects.size() > 0)   // Load and set external references
            {
                int[] xRef = new int[iLoadedObjects.size()];
                for (int i = 0; i < xRef.length; i++)
                    xRef[i] = ((Object3D)iLoadedObjects.elementAt(i)).handle;
                _setExternalReferences(handle, xRef);
            }
            else
            {
                throw new IOException("No external sections [" + iResourceName + "].");
            }
        }

        // Reset stream
        if (aStream instanceof PeekInputStream)
            ((PeekInputStream)aStream).rewind();
        else if (aStream.markSupported())
            aStream.reset(); // Reset is supported in ByteArrayInputStreams

        int read = 0;
        int size = aStream.available();

        if (size == 0)
        {
            size = 2048;    // start with some size
        }

        while (read < iTotalFileSize)
        {
            if (read + size > iTotalFileSize)
            {
                size = iTotalFileSize - read;
            }
            // Use native loader to load objects
            byte[] data = new byte[size];
            if (aStream.read(data) == -1)
            {
                break;
            }
            read += size;

            size = _decodeData(handle, 0, data);
            if (size > 0 && aStream.available() > size)
            {
                size = aStream.available();
            }
        }
        if (size != 0 || read != iTotalFileSize)
        {
            throw new IOException("Invalid file length [" + iResourceName + "].");
        }

        Object3D[] objects = null;
        int num = _getLoadedObjects(handle, null);
        if (num > 0)
        {
            int[] obj = new int[num];
            _getLoadedObjects(handle, obj);
            objects = new Object3D[num];
            for (int i = 0; i < objects.length; i++)
            {
                objects[i] = Interface.getObjectInstance(obj[i]);
            }
            setUserObjects();
        }
        return objects;
    }

    /**
     *
     */
    private void setUserObjects() throws IOException
    {
        int numObjects = _getObjectsWithUserParameters(handle, null);
        int[] obj = null;
        if (numObjects > 0)
        {
            obj = new int[numObjects];
            _getObjectsWithUserParameters(handle, obj);
        }
        for (int i = 0; i < numObjects; i++)
        {
            int num = _getNumUserParameters(handle, i);
            if (num > 0)
            {
                Hashtable hash = new Hashtable();
                for (int j = 0; j < num; j++)
                {
                    int len = _getUserParameter(handle, i, j, null);
                    byte[] data = new byte[len];
                    int id = _getUserParameter(handle, i, j, data);
                    if (hash.put(new Integer(id), data) != null)
                        throw new IOException("Duplicate id in user data [" + iResourceName + "].");;
                }
                Object3D object = Interface.getObjectInstance(obj[i]);
                object.setUserObject(hash);
            }
        }
    }

    /**
     * Load external resources
     */
    private void loadExternalRefs(InputStream aStream) throws IOException
    {
        // Check for the end of the aStream or file
        int firstByte = readByte(aStream);
        if (firstByte == -1 || (iTotalFileSize != 0 && iBytesRead >= iTotalFileSize))
        {
            return;
        }

        int compressionScheme = firstByte;

        int totalSectionLength = readUInt32(aStream);
        iBytesRead += totalSectionLength;
        if (aStream instanceof PeekInputStream && totalSectionLength > AVG_XREF_SEC_LENGTH)
            ((PeekInputStream)aStream).increasePeekBuffer(totalSectionLength - AVG_XREF_SEC_LENGTH);
        int uncompressedLength = readUInt32(aStream);
        int expectedCount = totalSectionLength;

        // Decompress data if necessary
        CountedInputStream uncompressedStream = null;
        if (compressionScheme == 0)
        {
            uncompressedStream = new CountedInputStream(aStream);
            if (uncompressedLength != totalSectionLength - 13)
            {
                throw new IOException("Section length mismatch [" + iResourceName + "].");
            }
        }
        else if (compressionScheme == 1)
        {
            if (uncompressedLength == 0 && totalSectionLength - 13 == 0)
            {
                uncompressedStream = new CountedInputStream(null);
            }
            else
            {
                if (uncompressedLength <= 0 || totalSectionLength - 13 <= 0)
                {
                    throw new IOException("Section length mismatch [" + iResourceName + "].");
                }
                byte[] compressed = new byte[(int) totalSectionLength - 13];
                aStream.read(compressed);

                byte[] uncompressed = new byte[(int) uncompressedLength];

                // zlib decompression
                if (!_inflate(compressed, uncompressed))
                {
                    throw new IOException("Decompression error.");
                }
                uncompressedStream = new CountedInputStream(
                    new ByteArrayInputStream(uncompressed));
            }
        }
        else
        {
            throw new IOException("Unrecognized compression scheme [" + iResourceName + "].");
        }

        // load all objects in this section
        uncompressedStream.resetCounter();

        while (uncompressedStream.getCounter() < uncompressedLength)
        {
            iLoadedObjects.addElement(loadObject(uncompressedStream));
        }

        if (uncompressedStream.getCounter() != uncompressedLength)
        {
            throw new IOException("Section length mismatch [" + iResourceName + "].");
        }

        // read checksum
        int checksum = readUInt32(aStream);
    }

    private Object3D loadObject(CountedInputStream aStream) throws IOException
    {
        int objectType = readByte(aStream);
        int length    = readUInt32(aStream);

        int expectedCount = aStream.getCounter() + length;
        Object3D newObject = null;

        if (objectType == 255)
        {
            String xref = readString(aStream);
            newObject = (new Loader(iFileHistory, iResourceName)).loadFromStream(xref)[0];
        }
        else
        {
            throw new IOException("Invalid external section [" + iResourceName + "].");
        }

        if (expectedCount != aStream.getCounter())
        {
            throw new IOException("Object length mismatch [" + iResourceName + "].");
        }

        return newObject;
    }

    /**
     * Read a byte integer from a stream
     */
    private static final int readByte(InputStream aStream) throws IOException
    {
        return aStream.read();
    }

    /**
     * Read a boolean from a stream
     */
    private static boolean readBoolean(InputStream aStream) throws IOException
    {
        int b = aStream.read();
        if (b == 0)
        {
            return false;
        }
        if (b != 1)
        {
            throw new IOException("Malformed boolean.");
        }
        return true;
    }

    /**
     * Read a unsigned integer from a stream
     */
    private static final int readUInt32(InputStream aStream) throws IOException
    {
        return aStream.read()
               + (aStream.read() << 8)
               + (aStream.read() << 16)
               + (aStream.read() << 24);
    }

    /**
     * Read a string from a stream
     */
    private static String readString(InputStream aStream) throws IOException
    {
        StringBuffer result = new StringBuffer();
        int i = 0;
        for (int c = aStream.read(); c != 0; c = aStream.read())
        {
            if ((c & 0x80) == 0)   // 0xxxxxxx => 1 byte
            {
                result.append((char)(c & 0x00FF));
            }
            else if ((c & 0xE0) == 0xC0)   // 110xxxxx => 2 bytes
            {
                int c2 = aStream.read();
                if ((c2 & 0xC0) != 0x80)   // second byte is not 10yyyyyy
                {
                    throw new IOException("Invalid UTF-8 string.");
                }
                else   // 110xxxxx 10yyyyyy
                {
                    result.append((char)(((c & 0x1F) << 6) | (c2 & 0x3F)));
                }
            }
            else if ((c & 0xF0) == 0xE0)   // 1110 xxxx => 3 bytes
            {
                int c2 = aStream.read();
                int c3 = aStream.read();
                if (((c2 & 0xC0) != 0x80) || // second byte is not 10yyyyyy
                        ((c3 & 0xC0) != 0x80))   // third byte is not 10zzzzzz
                {
                    throw new IOException("Invalid UTF-8 string.");
                }
                else   // 1110xxxx 10yyyyyy 10zzzzzz
                {
                    result.append((char)(((c & 0x0F) << 12) |
                                         ((c2 & 0x3F) <<6) |
                                         (c3 & 0x3F)));
                }
            }
            else   // none of above
            {
                throw new IOException("Invalid UTF-8 string.");
            }
        }

        return result.toString();
    }

    /**
     * Solve an identifier of the given data
     * @param aStream Stream
     * @return solved identifier.
     */
    private int getIdentifierType(InputStream aStream) throws IOException
    {
        byte[] data = new byte[MAX_IDENTIFIER_LENGTH];
        aStream.read(data);
        return getIdentifierType(data, 0);
    }

    /**
     * Solve an identifier of the given data
     * @param aData Data
     * @param aOffset Data offset
     * @return solved identifier.
     */
    private static int getIdentifierType(byte[] aData, int aOffset)
    {
        // Try the JPEG/JFIF identifier
        if (parseIdentifier(aData, aOffset, JPEG_FILE_IDENTIFIER))
        {
            return JPEG_TYPE;
        }
        // Try the PNG identifier
        else if (parseIdentifier(aData, aOffset, PNG_FILE_IDENTIFIER))
        {
            return PNG_TYPE;
        }
        // Try the M3G identifier
        else if (parseIdentifier(aData, aOffset, M3G_FILE_IDENTIFIER))
        {
            return M3G_TYPE;
        }
        return INVALID_HEADER_TYPE;
    }

    /**
     * Parse identifier from a data
     * @param aData Source data
     * @param aOffset Source data offset
     * @param aIdentifier Identifier
     * @return true if the data contains the given identifier
     */
    private static boolean parseIdentifier(byte[] aData, int aOffset, byte[] aIdentifier)
    {
        if ((aData.length - aOffset) < aIdentifier.length)
        {
            return false;
        }
        for (int index = 0; index < aIdentifier.length; index++)
        {
            if (aData[index + aOffset] != aIdentifier[index])
            {
                return false;
            }
        }
        return true;
    }

    /**
     * File name storage for preventing multiple referencing
     * @param name File name
     * @return true if the storage contains the given file name
     */
    private boolean inFileHistory(String name)
    {
        for (int i = 0; i < iFileHistory.size(); i++)
            if (((String)iFileHistory.elementAt(i)).equals(name))
            {
                return true;
            }
        return false;
    }

    /*
     * InputStream-related helper functions
     */

    /**
     * Open a HTTP stream and check its MIME type
     * @param name Resource name
     * @return a http stream and checks the MIME type
     */
    private InputStream getHttpInputStream(String name) throws IOException
    {
        InputConnection ic = (InputConnection)Connector.open(name);
        // Content-Type is available for http and https connections
        if (ic instanceof HttpConnection)
        {
            HttpConnection hc = (HttpConnection) ic;
            // Check MIME type
            String type = hc.getHeaderField("Content-Type");
            if (type != null &&
                    !type.equals("application/m3g") &&
                    !type.equals("image/png") &&
                    !type.equals("image/jpeg"))
            {
                throw new IOException("Wrong MIME type: " + type + ".");
            }
        }

        InputStream is;
        try
        {
            is = ic.openInputStream();
        }
        finally
        {
            try
            {
                ic.close();
                ic = null;
            }
            catch (Exception e) {}
        }
        return is;
    }

    // returns a stream built from the specified file or URI
    private InputStream getInputStream(String name) throws IOException
    {
        if (name.indexOf(':') != -1)   // absolute URI reference
        {
            return getHttpInputStream(name);
        }

        if (name.charAt(0) == '/')   // absolute file reference
        {
            return (new Object()).getClass().getResourceAsStream(name);
        }

        if (iParentResourceName == null)
        {
            throw new IOException("Relative URI.");
        }

        String uri = iParentResourceName.substring(0, iParentResourceName.lastIndexOf('/') + 1) + name;

        if (uri.charAt(0) == '/')
        {
            return (new Object()).getClass().getResourceAsStream(uri);
        }
        else
        {
            return getHttpInputStream(uri);
        }
    }

    class PeekInputStream extends InputStream
    {
        private int[] iPeekBuffer;
        private InputStream iStream;
        private int iBuffered;
        private int iCounter;

        PeekInputStream(InputStream aStream, int aLength)
        {
            iStream = aStream;
            iPeekBuffer = new int[aLength];
        }

        public int read() throws IOException
        {
            if (iCounter < iBuffered)
            {
                return iPeekBuffer[iCounter++];
            }

            int nv = iStream.read();

            if (iBuffered < iPeekBuffer.length)
            {
                iPeekBuffer[iBuffered] = nv;
                iBuffered++;
            }

            iCounter++;
            return nv;
        }

        public void increasePeekBuffer(int aLength)
        {
            int[] temp = new int[iPeekBuffer.length + aLength];
            for (int i = 0; i < iBuffered; i++)
                temp[i] = iPeekBuffer[i];
            iPeekBuffer = temp;
        }

        public int available() throws IOException
        {
            if (iCounter < iBuffered)
            {
                return iBuffered - iCounter + iStream.available();
            }
            return iStream.available();
        }

        public void close()
        {
            try
            {
                iStream.close();
            }
            catch (IOException ioe)
            {
                // Intentionally left empty
            }
        }

        public void rewind() throws IOException
        {
            if (iCounter > iBuffered)
            {
                throw new IOException("Peek buffer overrun.");
            }
            iCounter = 0;
        }
    }

    class CountedInputStream extends InputStream
    {
        private InputStream iStream;
        private int iCounter;

        public CountedInputStream(InputStream aStream)
        {
            iStream = aStream;
            resetCounter();
        }

        public int read() throws IOException
        {
            iCounter++;
            return iStream.read();
        }

        public void resetCounter()
        {
            iCounter = 0;
        }
        public int getCounter()
        {
            return iCounter;
        }

        public void close()
        {
            try
            {
                iStream.close();
            }
            catch (IOException ioe)
            {
                // Intentionally left empty
            }
        }

        public int available() throws IOException
        {
            return iStream.available();
        }
    }

//#ifdef RD_JAVA_OMJ
    private void doFinalize()
    {
        if (mFinalizer != null)
        {
            registeredFinalize();
            mFinalizer = null;
        }
    }
//#endif // RD_JAVA_OMJ

    // Finalization method for Symbian
    final private void registeredFinalize()
    {
        if (handle != 0)
        {
            Platform.finalizeObject(handle, iInterface);
            iInterface.deregister(this, iInterface);
            iInterface = null;
            handle = 0;
        }
    }

    // zlib decompression
    private native static boolean _inflate(byte[] data, byte[] buffer);

    // native loader
    private native static int _ctor(int handle);
    private native static int _decodeData(int handle, int offset, byte[] data);
    private native static void _setExternalReferences(int handle, int[] references);
    private native static int _getLoadedObjects(int handle, int[] objects);
    private native static int _getObjectsWithUserParameters(int handle, int[] objects);
    private native static int _getNumUserParameters(int handle, int obj);
    private native static int _getUserParameter(int handle, int obj, int index, byte[] data);
}
