/*
 *   
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

package com.sun.midp.chameleon.skins.resources;

import com.sun.midp.security.*;
import com.sun.midp.log.*;
import com.sun.midp.util.ResourceHandler;
import com.sun.midp.configurator.Constants;
import com.sun.midp.chameleon.skins.ScreenSkin;
import com.sun.midp.lcdui.DisplayAccess;
import com.sun.midp.io.j2me.storage.File;


// #ifndef ENABLE_CDC
import com.sun.midp.main.MIDletSuiteUtils;
// #endif

import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Display;

import java.util.*;
import java.io.*;

/** Utility class for skin resources. */
public class SkinResources {

    /** Constant to distinguish image resources with no index */
    private static final int NO_INDEX = -1;

    /**
     * Loaded skin resources. This object is used as resources
     * cache, so we don't have to load same resource again. In
     * MVM case this object is shared between Isolates, so all
     * Isolates can benefit from caching. However, only AMS
     * Isolate can put resources into the cache.
     */
    private static LoadedSkinResources resources;
    
    /**
     * Loaded skin properties. In MVM case, this object is shared
     * between Isolates.
     */
    private static LoadedSkinData skinData;

    /**
     * Inner class to request security token from SecurityInitializer.
     * SecurityInitializer should be able to check this inner class name.
     */
    static private class SecurityTrusted
        implements ImplicitlyTrustedClass {};

    /** A private internal reference to the system security token */
    private static SecurityToken secureToken =
        SecurityInitializer.requestToken(new SecurityTrusted());
    
    /**
     * A special internal reference to a tunnel which will provide
     * package access to Image inside of javax.microedition.lcdui.
     */
    private static ImageTunnel imageTunnel;

    /**
     * Display access helper class.
     */
    private static DisplayAccess displayAccess;

    /**
     * True, if skin is being loaded in AMS Isolate in MVM case,
     * false otherwise. Always true in SVM case.
     */
    private static boolean isAmsIsolate = true;

    /**
     * This class needs no real constructor, but its here as 'public'
     * so the SecurityIntializer can do a newInstance() on it and call
     * the initSecurityToken() method.
     */
    private SkinResources() {
    }

    /**
     * This method is called by the Display class to hand out Image
     * tunnel instance created in the public javax.microedition.lcdui
     * package and needed for romized images loading. 
     *
     * @param tunnel used to get images from the public LCDUI package
     */
    public static void initImageTunnel(ImageTunnel tunnel) {
        if (imageTunnel == null) {
            imageTunnel = tunnel;
        }
    }

    /**
     * This method is called by the Display class to initialize
     * Display access tunnel class.
     */
    public static void initDisplayAccess(DisplayAccess access) {
        displayAccess = access;
    }

    /**
     * This is method to be called before loadSkin to define the screen 
     * size while static initialization because no Display was created yet
     * and there is no DisplayAccessor at that stage.
     * @param width screen width
     * @param height screen height
     */
    public static void setScreenSize(int width, int height) {
        ScreenSkin.WIDTH = width;
        ScreenSkin.HEIGHT = height;
    }

    /**
     * Load the skin, including all its properties and images. Some parts
     * of the skin may be lazily initialized, but this method starts the
     * process. If the flag to 'reload' is true, the method will ignore
     * all previously loaded resources and go through the process again.
     *
     * @param reload if true, ignore previously loaded resources and reload
     * @throws IOException if there was error reading skin data file
     * @throws IllegalStateException if skin data file is invalid
     */
    public static void loadSkin(boolean reload) 
        throws IllegalStateException, IOException {

        resources = null;
        skinData = null;

// #ifndef ENABLE_CDC
        isAmsIsolate = MIDletSuiteUtils.isAmsIsolate();
// #endif

        if (reload) {
            if (Logging.REPORT_LEVEL <= Logging.CRITICAL) {
                Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI,
                        "Skin reloading isn't implemented yet");
            }
        }
        
        if (!isAmsIsolate) {
            resources = (LoadedSkinResources)getSharedResourcePool();
            skinData = (LoadedSkinData)getSharedSkinData();
        } else {
            resources = new LoadedSkinResources();

            skinData = new LoadedSkinData();
            String fileName = 
                File.getConfigRoot(Constants.INTERNAL_STORAGE_ID) + 
                SkinResourcesConstants.SKIN_BINARY_FILE_NAME;

            skinData.readFromFile(fileName);

            resources.fonts = new Font[skinData.fontValues.length];
            resources.images = new Image[skinData.imageValues.length];
            
            // For MVM, share resources cache and loaded properties
            // between Isolates. For SVM, those methods do nothing.
            shareResourcePool(resources);
            shareSkinData(skinData);
        }
        
        
        // After reading in the properties from storage (either ROM
        // image or filesystem, we establish all the individual values
        // in the various properties classes
        boolean loadAll = ifLoadAllResources();
        loadResources(loadAll);
        
        // NOTE: The remaining properties classes need to be set and
        // possibly re-loaded lazily. 
        
        // IMPL_NOTE if (reload), need to signal the LF classes in a way they
        // will re-load their skin resources on demand
    }

    /**
     * Utility method used by skin property classes to load
     * image resources.
     * 
     * @param identifier a unique identifier for the image property
     * @return the Image if one is available, null otherwise
     */
    public static Image getImage(int identifier) {
        return getImage(identifier, NO_INDEX);
    }
    
    /**
     * Utility method used by skin property classes to load
     * image resources.
     * 
     * @param identifier a unique identifier for the image property
     * @param index index of the image
     *
     * @return the Image if one is available, null otherwise
     */
    public static Image getImage(int identifier, int index) {
        int imageIdx = 0;
        String imageIdentifier = null;
        Image image = null;
        boolean isLoaded = false;
        
        // If we're set not to bother loading images, just return null
        if (!Constants.CHAM_USE_IMAGES) {
            return null;
        }

        try {
            imageIdx = skinData.properties[identifier];
            if (index != NO_INDEX) {
                imageIdx += index;
            }

            // Check if this resource alread has been loaded
            image = resources.images[imageIdx];
        
            if (image != null) {
                // image already has been loaded
                isLoaded = true;
            } else {
                byte[] imageData;
           
                imageIdentifier = skinData.imageValues[imageIdx];
                if (imageIdentifier.length() == 0) {
                    // image hasn't been specified
                    return null;
                }

                if (imageIdentifier == null) {
                    if (Logging.REPORT_LEVEL <= Logging.CRITICAL) {
                        Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI, 
                            "No value for skin property: id = " + identifier);
                    }

                    return null;
                }

                // Try to load the romized image
                if (Constants.CHAM_ROMIZED_IMAGES && imageTunnel != null) {
                    int romIndex = skinData.romizedImageIndexes[imageIdx];
                    if (romIndex != -1) {
                        int imageDataPtr = getRomizedImageDataArrayPtr(
                                romIndex);
                        int imageDataLength = getRomizedImageDataArrayLength(
                                romIndex);
                        image = imageTunnel.getRomizedImage(imageDataPtr, 
                                imageDataLength);
                    }
                } 
                
                // Image is not romized, load from filesystem
                if (image == null) {
                    // Try to load the PNG image from the filesystem
                    imageData = ResourceHandler.getSystemResource(
                            secureToken, imageIdentifier + ".png");

                    // Try to load the raw image from the filesystem
                    if (imageData == null) {
                        imageData = ResourceHandler.getSystemResource(
                                secureToken, imageIdentifier + ".raw");
                    }

                    if (imageData != null) {
                        image = Image.createImage(
                                imageData, 0, imageData.length);
                    }
                }
            }
        } catch (Throwable t) {
            if (Logging.REPORT_LEVEL <= Logging.CRITICAL) {
                Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI,
                        "Exception caught while loading Image resource " +
                        imageIdentifier + ": " + t);
            }
        }
        
        if (image != null) {
            if (!isLoaded && isAmsIsolate) {
                // add resource to shared resources pool
                resources.images[imageIdx] = image;
            }
        } else {
            if (Logging.REPORT_LEVEL <= Logging.CRITICAL) {
                Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI,
                        "No resource found when loading Image resource: " + 
                        imageIdentifier);
            }
        }

        return image;
    }

    /**
     * Utility method used by skin property classes to load
     * composite image resources consisting of a few images.
     *
     * @param identifier a unique identifier for the composite image property
     * @param piecesNumber number of pieces consisting the composite image 
     *
     * @return the Image[] with loaded image pieces,
     * or null if some of the pieces is not available
     */
    public static Image[] getCompositeImage(
            int identifier, int piecesNumber) {
        Image[] result = new Image[piecesNumber];
        for (int i = 0; i < piecesNumber; i++) {
            result[i] = getImage(identifier, i);
            if (result[i] == null) {
                result = null;
                break;
            }
        }
        return result;
    }

    /**
     * Utility method used by skin property classes to load
     * Font resources.
     *
     * @param identifier a unique identifier for the Font property
     * @return the Font object or null in case of error
     */     
    public static Font getFont(int identifier) {
        int fontIdx = 0;
        int fontIdentifier = 0;
        Font font = null;
        boolean isLoaded = false;

        try {
            fontIdx = skinData.properties[identifier];

            // Check if this resource alread has been loaded
            font = resources.fonts[fontIdx];
            
            if (font != null) {
                isLoaded = true;
            } else {
                fontIdentifier = skinData.fontValues[fontIdx];
                font = FontResources.getFont(fontIdentifier);
            }
        } catch (Exception e) {
            if (Logging.REPORT_LEVEL <= Logging.CRITICAL) {
                Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI,
                        "Exception caught while loading Font resource: " + 
                        identifier + ": " + e);
            }
        }
            
        if (font != null) {
            if (!isLoaded && isAmsIsolate) {
                // add resource to shared resources pool
                resources.fonts[fontIdx] = font;
            }
        } else {
            if (Logging.REPORT_LEVEL <= Logging.CRITICAL) {
                Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI,
                        "No resource found when loading Font resource: " + 
                        fontIdentifier);
            }

            font = FontResources.getFont(500);
        }

        return font;
    }
    
    /**
     * Utility method used by skin property classes to load
     * String resources.
     *
     * @param identifier a unique identifier for the String property
     * @return the String object or null in case of error
     */     
    public static String getString(int identifier) {
        try {
            int stringIdx = skinData.properties[identifier];
            return skinData.stringValues[stringIdx];
        } catch (Exception e) {
            if (Logging.REPORT_LEVEL <= Logging.CRITICAL) {
                Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI,
                        "Exception caught while loading String resource: " + 
                        identifier + ": " + e);
            }
            return null;
        }
    }
    
    /**
     * Utility method used by skin property classes to load
     * integer resources.
     *
     * @param identifier a unique identifier for the integer property
     * @return an integer or -1 in case of error
     */     
    public static int getInt(int identifier) {
        try {
            return skinData.properties[identifier];
        } catch (Exception e) {
            if (Logging.REPORT_LEVEL <= Logging.CRITICAL) {
                Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI,
                        "Exception caught while loading integer resource: " +
                        identifier + ": " + e);
            }            
            return -1;
        }        
    }
    
    /**
     * Returns sequence of integer numbers corresponding to 
     * specified property identifer.
     *
     * @param identifier a unique identifier for the property
     * @return the int[] representing the sequence or null in case of error
     */
    public static int[] getNumbersSequence(int identifier) {
        try {
            int seqIdx = skinData.properties[identifier];
            int totalNumbers = skinData.intSeqValues[seqIdx];
            seqIdx += 1;
            int[] nums = new int[totalNumbers];
            for (int i = 0; i < totalNumbers; ++i) {
                nums[i] = skinData.intSeqValues[seqIdx + i];
            }
            
            return nums;
        } catch (Exception e) {
            Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI,
                    "Exception caught while loading integers sequence " + 
                    "resource: " + identifier + ": " + e);
        }
        return null;
    }    

    /**
     * Translates constants composition from SkinResourcesConstants class
     * into corresponding constants composition from Graphics class.
     *
     * @param num constants composition
     * @return translated composition
     */
    static int resourceConstantsToGraphics(int num) {
        if (num == SkinResourcesConstants.SOLID) {
            return Graphics.SOLID;
        } else if (num == SkinResourcesConstants.DOTTED) {
            return Graphics.DOTTED;
        }

        int rv = 0;
        if ((num & SkinResourcesConstants.TOP) != 0) {
            rv |= Graphics.TOP;
        }
        if ((num & SkinResourcesConstants.LEFT) != 0) {
            rv |= Graphics.LEFT;
        }
        if ((num & SkinResourcesConstants.BOTTOM) != 0) {
            rv |= Graphics.BOTTOM;
        }
        if ((num & SkinResourcesConstants.RIGHT) != 0) {
            rv |= Graphics.RIGHT;
        }

        return rv;
    }

    /**
     * Load resources data.
     * 
     * @param loadAll if true, load all resources. Otherwise, 
     * load only selected resources. The rest will be loaded 
     * lazily.
     */
    private static void loadResources(boolean loadAll) {
        // load selected resources
        ScreenResources.load();
        ScrollIndResources.load();
        SoftButtonResources.load();
        TickerResources.load();
        TitleResources.load();
        AlertResources.load();

        // load the rest of resources
        if (loadAll) {
            PTIResources.load();
            BusyCursorResources.load();
            ChoiceGroupResources.load();
            DateEditorResources.load();
            DateFieldResources.load();
            GaugeResources.load();
            ImageItemResources.load();
            MenuResources.load();
            ProgressBarResources.load();
            StringItemResources.load();
            TextFieldResources.load();
            UpdateBarResources.load();
        }
    }

    /**
     * Refresh skin after screen size is changed
     */
    public static void adjustScreen() {

        setScreenSize(displayAccess.getDisplayWidth(), 
                      displayAccess.getDisplayHeight());
        SoftButtonResources.load(true);
        AlertResources.load(true);
    }

    /**
     * Native method to store a strong cross-isolate reference
     * to the resource pool Object. This will allow the CSkin
     * class in all isolates to share the same loaded resources.
     * @param pool object reference to store in the resource pool
     */
    private native static void shareResourcePool(Object pool);
    
    /**
     * Native method to retrieve the cross-isolate resource 
     * pool Object. This will allow the CSkin class in all
     * isolates to share the same loaded resources.
     * @return a shared resource pool object reference
     */
    private native static Object getSharedResourcePool();

    /**
     * Native method to store a strong cross-isolate reference
     * to the skin properties Object. This will allow the CSkin
     * class in all isolates to share skin properties.
     * @param props object reference to store 
     */
    private native static void shareSkinData(Object props);
    
    /**
     * Native method to retrieve the cross-isolate skin 
     * properties Object. This will allow the CSkin class 
     * in all isolates to share skin properties.
     * @return a shared skin properties object reference
     */
    private native static Object getSharedSkinData();
    
    /**
     * Determine how skin resources should be loaded: at once, 
     * during skin loading, or lazily, on first use.
     * @return true if all resources should be loaded at once.
     */
    private native static boolean ifLoadAllResources();

    /**
     * Returns native pointer to romized image data array as Java int
     *
     * @param imageId romized image id
     * @return native pointer to romized image data array as Java int,
     * if image is not romized the return value is 0
     */
    private native static int getRomizedImageDataArrayPtr(int imageId);

    /**
     * Returns length of native romized image data array
     *
     * @param imageId romized image id
     * @return length of native romized image data array,
     * if image is not romized the return value is -1
     */
    private native static int getRomizedImageDataArrayLength(int imageId);
}

/**
 * Class for caching loaded resources. Because we know about 
 * all resources in advance, we can assign integer value to 
 * each resource. This value is used as index into arrays below.
 */
final class LoadedSkinResources {
    /** Loaded fonts */
    Font[] fonts;

    /** Loaded images */
    Image[] images;
    
// #ifdef ENABLE_CDC
    protected native void finalize();
// #else
    private native void finalize();
// #endif
    
}

/**
 * Class reperesenting loaded skin data file.
 */
final class LoadedSkinData {
    /**
     * Skin properties. Each property has an integer value associated
     * with it, which is used as index into this array. The meaning of 
     * value in this array depends of the property type. 
     * For integer property, the value stored in array is the value of 
     * property itself.
     * For string property, the value stored in array is an index into 
     * another array, where string values are stored.
     * For image property, the value stored in array is an index into 
     * another array, where images identifiers are stored.
     * For font property, the value stored in array is an index into
     * another array, where fonts indentifiers are stored.
     * For integer numbers sequence property, the value stored in array
     * is an index into another array, where integer numbers sequences
     * are stored.
     */
    int[] properties;

     /**
     * Array with integer numbers sequences for integer numbers
     * sequence properties. Each sequence is stored as number of
     * integers in sequence followed by integers themself.
     */
    int[] intSeqValues;

    /**
     * Array with values for string properties.
     */
    String[] stringValues;
   
    /**
     * Array with font indentifiers for font properties.
     */
    int[] fontValues;

    /**
     * Array with images identifiers for image properties.
     */
    String[] imageValues;

    /**
     * Romized image indexes. -1, if image isn't romized. 
     */
    int[] romizedImageIndexes;

    /**
     * For accessing constant values from native code
     */

    /** US ASCII string encoding */
    static byte STRING_ENCODING_USASCII = 
        SkinResourcesConstants.STRING_ENCODING_USASCII;

    /** UTF8 string encoding */
    static byte STRING_ENCODING_UTF8 = 
        SkinResourcesConstants.STRING_ENCODING_UTF8;

// #ifdef ENABLE_CDC
    protected native void finalize();
// #else
    private native void finalize();
// #endif

    LoadedSkinData() {
        properties = null;
        intSeqValues = null;
        stringValues = null;
        fontValues = null;
        imageValues = null;
        romizedImageIndexes = null;
    }

    /**
     * Reads skin data from file.
     *
     * @param fileName name of the file to read data from
     * @throws IllegalStateException if file fails some checks
     */
    void readFromFile(String fileName) 
        throws IOException, 
               IllegalStateException {

        int[] properties;
        int[] intSeqValues;
        String[] stringValues;
        int[] fontValues;
        String[] imageValues;
        int[] romizedImageIndexes;

        try {
            // prepare for reading data from skin file
            beginReadingSkinFile(fileName);

            // check file validity
            checkMagicSequence();
            checkVersion();

            // read actual data
            properties = readIntArray();
            intSeqValues = readIntArray();
            stringValues = readStringArray();
            fontValues = readIntArray();
            imageValues = readStringArray();
            romizedImageIndexes = readIntArray();

            this.properties = properties;
            this.intSeqValues = intSeqValues;
            this.stringValues = stringValues;
            this.fontValues = fontValues;
            this.imageValues = imageValues;
            this.romizedImageIndexes = romizedImageIndexes;
        } finally {
            // cleanup after reading skin file
            finishReadingSkinFile();
        }
    }

    /**
     * Prepares for reading data from skin file
     */
    private native static void beginReadingSkinFile(String fileName)
        throws IOException;

    /**
     * Reads byte array from skin file
     *
     * @param length array length
     * @return int[] array read
     */
    private native static byte[] readByteArray(int length)
        throws IOException;

    /**
     * Reads integer array from skin file
     *
     * @return int[] array read
     */
    private native static int[] readIntArray()
        throws IOException;

    /**
     * Reads String array from skin file
     *
     * @return String[] array read
     */
    private native static String[] readStringArray() 
        throws IOException;


    /**
     * Cleanups after reading data from skin file
     */
    private native static int finishReadingSkinFile()
        throws IOException;

    /**
     * Checks if file contains valid magic sequence
     *
     * @throws IllegalStateException if magic sequence is invalid
     */
    private void checkMagicSequence() 
        throws IllegalStateException, IOException {

        int magicLength = SkinResourcesConstants.CHAM_BIN_MAGIC.length;
        byte[] supposedMagic = readByteArray(magicLength);

        for (int i = 0; i < magicLength; ++i) {
            byte expected = (byte)SkinResourcesConstants.CHAM_BIN_MAGIC[i];
            if (supposedMagic[i] != expected) {
                if (Logging.REPORT_LEVEL <= Logging.CRITICAL) {
                    Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI,
                        "Skin data file has invalid magic sequence");
                }
                throw new IllegalStateException();
            }
        }
    }

    /**
     * Checks if file contains valid format version number
     *
     * @throws IllegalStateException if version number is invalid
     */
    private void checkVersion() 
        throws IllegalStateException, IOException {

        int[] ver = readIntArray();
        if (ver.length != 1 ||
            ver[0] != SkinResourcesConstants.CHAM_BIN_FORMAT_VERSION) {

            if (Logging.REPORT_LEVEL <= Logging.CRITICAL) {
                Logging.report(Logging.CRITICAL, LogChannels.LC_HIGHUI,
                    "Skin data stream has invalid format version: " + ver[0]);
            }
            throw new IllegalStateException();
        }
    }

}
