/*
 *
 *
 * 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.content;

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

import com.sun.midp.security.Permissions;
import com.sun.midp.security.SecurityToken;

import com.sun.midp.midlet.MIDletSuite;
import com.sun.midp.main.MIDletSuiteUtils;
import com.sun.midp.midlet.MIDletStateHandler;
import com.sun.midp.midletsuite.MIDletSuiteImpl;
import com.sun.midp.midletsuite.MIDletSuiteStorage;
import com.sun.midp.midletsuite.MIDletSuiteLockedException;
import com.sun.midp.midletsuite.MIDletSuiteCorruptedException;

import com.sun.midp.events.NativeEvent;
import com.sun.midp.events.EventTypes;
import com.sun.midp.events.EventQueue;

import com.sun.midp.io.Util;

/**
 * Each AppProxy instance provides access to the AMS information
 * and functions for a running or installed application.
 * This class must be replaced for each platform/profile combination and
 * be integrated with the appropriate Applicaiton Management Software.
 * <p>
 * The AppProxy class is *only* available within
 * this package for security purposes.
 * An instance exists for the current application and can
 * be retrieved for any installed application.
 * The following methods provide functions needed by CHAPI:
 * <ul>
 * <li>{@link #getCurrent} - the current application and the main
 *     class within it.
 * <li>{@link #forApp} - the AppProxy for a named  Jar ID and
 *     classname.  For MIDP, they are the suiteID of the storage and
 *     MIDlet within the JAR.
 * <li>{@link #getStorageID} - the storage ID for this AppProxy.
 * <li>{@link #getClassname} - the classname for this AppProxy.
 * <li>{@link #getApplicationID} - the CHAPI defined unique identifier
 *     for the application
 * <li>{@link #getApplicationName} = a user friendly name for this application.
 * <li>{@link #getAuthority} - the authority under which the application
 *     is granted permissions. For MIDP, the subject field of the signing
 *     certificate.
 * <li>{@link #getProperty} - access to the properties in the manifest or
 *     application descriptor.
 * <li>{@link #getVersion} - the version number of this application.
 * <li>{@link #getDefaultID} - the default applicationID if none is provided
 *     in the manifest or application descriptor.
 * <li>{@link #checkRegisterPermission} - method to check if the caller
 *     is allowed to register or unregister handlers.
 * <li>{@link #checkAPIPermission} - method to check if the caller has
 *     permission to use internal APIs. Caller must have an appropriate
 *     security token (depending on the platform)
 * <li>{@link #isRegistered} - used to check if the application is
 *     registered as appropriate for the platform. For MIDP, there
 *     is a MIDlet-n attribute in manifest or JAD.
 * <li>{@link #verifyApplication} - Verify that a named class is the
 *     correct type and access to be launched as an application in the
 *     profile.  For MIDP, the class must extend MIDlet.
 * <li>{@link #launch} - Request this application be launched if it is
 *     not already running.  Return a boolean indicating if the current
 *     application MUST voluntarily exit before the launched app can run.
 * <li>{@link #requestForeground} - ask the "window manager" to give the
 *     foreground to the requested application.
 * <
 * </ul>
 */
class AppProxy {
    /** This class has a different security domain than the MIDlet suite */
    private static SecurityToken classSecurityToken;

    /** The current AppProxy. */
    private static AppProxy currentApp;

    /** The log flag to enable informational messages. */
    static final boolean LOG_INFO = false;

    /**
     * This global state is <code>true</code> if an application
     * has been executed and this application should be exiting
     * to let it run. For SVM, this is set true by {@link #launch }
     * and stays that way.
     * For MVM, it is cleared if the VM allows the execute to occur.
     * At MIDlet exit, {@link InvocationImpl#invokeNext} checks to see
     * if another application Invoction is pending and invokes it.
     * If this application has NOT invoked anything then the
     * chosen app will be invoked.
     */
    private static boolean oneExecute;

    /** The known AppProxy instances. Key is classname. */
    protected Hashtable appmap;

    /** The mutex used to avoid corruption between threads. */
    protected static final Object mutex = new Object();

    /** The MIDlet suite for this app. */
    protected final MIDletSuite msuite;

    /** The storageId (suiteId) for this application. */
    protected final int storageId;

    /** The classname of the application. */
    protected String classname;

    /** The application name. */
    private String applicationName;

    /** The ApplicationID, (same a suiteId). */
    private String applicationID;

    /** The application is registered. */
    private boolean isRegistered;

    /** MIDlet property for the suite version. */
    static final String VERSION_PROP       = "MIDlet-Version";

    /** MIDlet property for the suite vendor. */
    static final String VENDOR_PROP        = "MIDlet-Vendor";

    /**
     * Sets the security token used for priveleged operations.
     * The token may only be set once.
     * @param token a Security token
     */
    static void setSecurityToken(SecurityToken token) {
        if (classSecurityToken != null) {
            throw new SecurityException();
        }
        classSecurityToken = token;
    }

    /**
     * Gets the AppProxy for the currently running application.
     * @return the current application.
     */
    static AppProxy getCurrent() {
        synchronized (mutex) {
            if (currentApp == null) {
                MIDletStateHandler mh =
                    MIDletStateHandler.getMidletStateHandler();
                MIDletSuite msuite = mh.getMIDletSuite();
                try {
                    currentApp = (msuite != null) ?
                        new AppProxy(msuite,
                                     mh.getFirstRunningMidlet(),
                                     null) :
                        new AppProxy(MIDletSuite.INTERNAL_SUITE_ID, "");
                } catch (ClassNotFoundException cnfe) {
                    return null;
                }
            }
        }
        return currentApp;
    }

    /**
     * Construct an AppProxy with the specified MIDletSuite.
     *
     * @param msuite the MIDletSuite to initialize.
     * @param classname the classname of a copackaged application.
     * @param appmap a Hashtable to keep track of instances; may be null
     * @exception ClassNotFoundException if the <code>classname</code>
     *   is not present
     * @exception IllegalArgumentException if classname is not
     *   a valid application
     */
    protected AppProxy(MIDletSuite msuite, String classname, Hashtable appmap)
        throws ClassNotFoundException
    {
        if (appmap == null) {
            // Allocate a Hashtable if needed
            appmap = new Hashtable();
        }

        this.msuite = msuite;
        this.storageId = msuite.getID();
        this.classname = classname;
        this.appmap = appmap;
        if (classname != null) {
            verifyApplication(classname);
            initAppInfo();
            appmap.put(classname, this);
            if (LOG_INFO) {
                logInfo("AppProxy created: " + classname);
            }
        }
    }

    /**
     * Construct an AppProxy with the specified suiteId, classname.
     * This is just a placeholder to make launch work.
     *
     * @param storageId the suiteId
     * @param classname the classname
     */
    protected AppProxy(int storageId, String classname)
    {
        this.storageId = storageId;
        this.classname = classname;
        MIDletSuite midletSuite = null;

        /* trying to initialize fields from suite info */
        try {
            midletSuite = MIDletSuiteStorage.
                    getMIDletSuiteStorage(classSecurityToken).
                    getMIDletSuite(storageId, false);
        } catch (MIDletSuiteLockedException msle) {
            if (LOG_INFO) {
                logException("AppProxy creation fails", msle);
            }
        } catch (MIDletSuiteCorruptedException msce) {
            if (LOG_INFO) {
                logException("AppProxy creation fails", msce);
            }
        }
        msuite = midletSuite;

        if (msuite != null) {
            initAppInfo();
        }

        if (LOG_INFO) {
            logInfo("AppProxy created: " + classname);
        }
    }

    /**
     * Gets the AppProxy for an application class in the current bundle.
     * @param classname the name of the application class
     * @return the AppProxy for classname; <code>null</code> if not
     * a valid application (MIDlet)
     * @exception ClassNotFoundException if the <code>classname</code>
     *   is not present
     * @exception IllegalArgumentException if classname is
     *  not a valid application
     */
    AppProxy forClass(String classname) throws ClassNotFoundException
    {
        AppProxy curr = null;
        synchronized (mutex) {
            // Check if class already has a AppProxy
            curr = (AppProxy)appmap.get(classname);
            if (curr == null) {
                // Create a new instance
                // throws ClassNotFoundException and IllegalArgumentException
                curr = new AppProxy(msuite, classname, appmap);
            }
        }
        return curr;
    }

    /**
     * Gets the AppProxy for an storageID and classname.
     * @param storageId the storageId (suiteId)
     * @param classname the name of the application class
     * @return the AppProxy for suiteId, classname;
     *   <code>null</code> if not a valid application (MIDlet)
     * @exception ClassNotFoundException if the <code>classname</code>
     * is not present
     * @exception IllegalArgumentException if classname is not
     *   a valid application
     */
    AppProxy forApp(int storageId, String classname)
        throws ClassNotFoundException
    {
        // Check in the current suite
        if (storageId == this.storageId) {
            return forClass(classname);
        }

        // Create a new instance
        AppProxy curr = new AppProxy(storageId, classname);

        return curr;
    }

    /**
     * Gets the storage ID of this application.
     * The ID uniquely identifies the package/application bundle.
     * @return the application ID.
     */
    int getStorageId() {
        return storageId;
    }

    /**
     * Gets the classname of this application.
     * @return the classname
     */
    String getClassname() {
        return classname;
    }

    /**
     * Gets the user friendly application name.
     * @return the user friendly application name
     */
    String getApplicationName() {
        return applicationName;
    }

    /**
     * Gets the CHAPI application ID for this application.
     * @return the CHAPI application ID.
     */
    String getApplicationID() {
        return applicationID;
    }

    /**
     * Gets the version string for the application.
     * @return the version
     */
    String getVersion() {
        return msuite == null? null: msuite.getProperty(VERSION_PROP);
    }

    /**
     * Gets the Trusted authority that authenticated this application.
     * <p>
     * For MIDP, this is the CA of the signer.
     * If exception is thrown during getting authorization
     * this methods ignores it and returns null.
     * @return the authority.
     */
    String getAuthority() {
        String auth = null;
        try {
            auth = ((MIDletSuiteImpl)msuite).getInstallInfo().getCA();
        } catch (RuntimeException e) {
        }
        return auth;
    }

    /**
     * Gets true if the application is a registered application.
     * <p>
     * for MIDP, this means there was a MIDlet-n attribute.
     * @return true if this application is registered
     */
    boolean isRegistered() {
        return isRegistered;
    }

    /**
     * Gets a property from the manifest or application descriptor.
     * @param key the name of the property to retrieve
     * @return the value of the property or <code>null</code>
     */
    String getProperty(String key) {
        return msuite.getProperty(key);
    }

    /**
     * Check the permission to register or unregister.
     * @param reason the reason for the permission check
     * @exception SecurityException if not allowed
     */
    final void checkRegisterPermission(String reason) {
        try {
            msuite.checkForPermission(Permissions.CHAPI_REGISTER,
                                      getApplicationName(), reason);
        } catch (InterruptedException ie) {
            throw new SecurityException("interrupted");
        }
    }

    /**
     * Check if the internal API use is allowed.
     * @param securityToken a generic security token
     * @exception SecurityException thrown if internal API use not allowed
     */
    final static void checkAPIPermission(Object securityToken) {
        if (securityToken != null) {
            ((SecurityToken)securityToken).
                checkIfPermissionAllowed(Permissions.MIDP);
        } else {
            MIDletSuite msuite =
                MIDletStateHandler.getMidletStateHandler().getMIDletSuite();
            if (msuite != null) {
                msuite.checkIfPermissionAllowed(Permissions.AMS);
            }
        }
    }

    /**
     * Request the transition of the foreground to this application
     * from the invoking application.
     * @param invokingSuiteId the invoking suiteId
     * @param invokingClassname the invoking classname
     * @param targetSuiteId the target suiteId
     * @param targetClassname the target classname
     */
    static void requestForeground(int invokingSuiteId,
                                  String invokingClassname,
                                  int targetSuiteId,
                                  String targetClassname)
    {
        NativeEvent event =
            new NativeEvent(EventTypes.FOREGROUND_TRANSFER_EVENT);
        event.intParam1 = invokingSuiteId;
        event.stringParam1 = invokingClassname;
        event.intParam2 = targetSuiteId;
        event.stringParam2 = targetClassname;

        int amsIsolateId = MIDletSuiteUtils.getAmsIsolateId();
        EventQueue eventQueue = EventQueue.getEventQueue(classSecurityToken);
        eventQueue.sendNativeEventToIsolate(event, amsIsolateId);
    }

    /**
     * The stronger variant for request the transition of
     * the foreground to this application.
     * @param targetSuiteId the target suiteId
     * @param targetClassname the target classname
     */
    static void requestForeground(int targetSuiteId,
                                  String targetClassname)
    {
        NativeEvent event =
            new NativeEvent(EventTypes.SET_FOREGROUND_BY_NAME_REQUEST);
        event.intParam1 = targetSuiteId;
        event.stringParam1 = targetClassname;

        int amsIsolateId = MIDletSuiteUtils.getAmsIsolateId();
        EventQueue eventQueue = EventQueue.getEventQueue(classSecurityToken);
        eventQueue.sendNativeEventToIsolate(event, amsIsolateId);
    }

    /**
     * Launch this application.
     * Don't launch another application unless
     * the execute allows this application to continue after
     * the launch.
     * <p>
     * In SVM, (sequential applications) only the first
     * execute matters; later ones should not override the
     * first.  All pending Invocations are queued in InvocationStore
     * so they will not be lost.  When MIDlets exit, another
     * application will be selected from those pending.
     *
     * @param displayName name to show to the user of what to launch
     * @return <code>true</code> if the application must exist
     *  before the target application can proceed.
     */
    boolean launch(String displayName) {
        /*
         * If an execute has been queued already; don't queue another
         */
        if (oneExecute) {
            // launched something previously and app should exit.
            if (LOG_INFO) {
                logInfo("Launch skipped: " + classname +
                        ", oneExecute: " + oneExecute);
            }
            return true;
        } else {
            /* Invoke the target application */
            oneExecute =
                MIDletSuiteUtils.execute(classSecurityToken,
                                          storageId,
                                          classname, displayName);
            if (LOG_INFO) {
                logInfo("Launch: " + classname +
                        ", oneExecute: " + oneExecute);
            }
            return oneExecute;
        }
    }


    /**
     * Verify that the classname is a valid application.
     * It must extend MIDlet.
     * @param classname the application class
     *
     * @exception ClassNotFoundException is thrown if the class cannot be found
     * @exception IllegalArgumentException if the classname is null or empty
     *  or does not implement the lifecycle of a MIDlet.
     */
    protected void verifyApplication(String classname)
        throws ClassNotFoundException
    {
        /* check the classname for null and get the class */
        Class appClass = Class.forName(classname);
        Class midletClass = Class.forName("javax.microedition.midlet.MIDlet");
        if ((!midletClass.isAssignableFrom(appClass)) ||
            appClass == midletClass) {
            throw new IllegalArgumentException("not a MIDlet");
        }
    }

    /**
     * Initialize application name and application ID
     * from the attributes.
     */
    protected void initAppInfo() {
        // Check if it is an internal MIDlet
        if (storageId == MIDletSuite.INTERNAL_SUITE_ID) {
            applicationName = classname.substring(classname.lastIndexOf('.') + 1);
            applicationID = "system";
            isRegistered = true;
            return;
        }

        // Check if a registered MIDlet
        String[] minfo = getMIDletInfo(msuite, classname);

        // if a MIDlet, set the application name and application ID
        if (minfo != null) {
            applicationName = minfo[0];
            applicationID = minfo[2];
            isRegistered = true;
        }

        // Fill in defaults for appName and applicationID
        if (applicationName == null || applicationName.length() == 0) {
            applicationName = msuite.getProperty(
                                        MIDletSuiteImpl.SUITE_NAME_PROP);
        }

        if (applicationID == null || applicationID.length() == 0) {
            applicationID = getDefaultID();
        }
    }

    /**
     * Gets the content handler ID for the current application.
     * The ID uniquely identifies the application which contains the
     * content handler.
     * The application ID is assigned when the application is installed.
     * <p>
     * All installed applications have vendor and name;
     *
     * @return the ID; MUST NOT be <code>null</code>
     */
    String getDefaultID() {
        StringBuffer sb = new StringBuffer(80);
        String s = msuite.getProperty(VENDOR_PROP);
        sb.append((s != null) ? s : "internal");
        sb.append('-');
        s = msuite.getProperty(MIDletSuiteImpl.SUITE_NAME_PROP);
        sb.append((s != null) ? s : "system");
        sb.append('-');
        sb.append(classname);
        return sb.toString().replace(' ', '_');
    }

    /**
     * Get the MIDletInfo for the named MIDlet.
     * @param suite the MIDlet suite to look in for the midlet
     * @param classname the class name to look for
     * @return an array of Strings, name, icon, ID
     *  null if there is no matching MIDlet-n.
     */
    private static String[] getMIDletInfo(MIDletSuite suite, String classname)
    {
        for (int i = 1; ; i++) {
            String midletn = "MIDlet-".concat(Integer.toString(i));
            String attr = suite.getProperty(midletn);
            if (attr == null) {
                break; // break out of loop, not found
            }

            Vector args = Util.getCommaSeparatedValues(attr);
            if (args.size() < 3) {
                // Not enough args to be legit
                continue;
            }

            if (!classname.equals(args.elementAt(2))) {
                continue;
            }
            String[] values = new String[args.size()];
            args.copyInto(values);

            String ID = suite.getProperty(midletn.concat("-ID"));
            values[2] = ID;

            return values;
        }
        return null;
    }

    /**
     * Log an information message to the system logger for this AppProxy.
     * @param msg a message to write to the log.
     */
    void logInfo(String msg) {
        if (LOG_INFO) {
            System.out.println(">> " + threadID() + ": " + msg);
        }
    }

    /**
     * Log an information message to the system logger for this AppProxy.
     * @param msg a message to write to the log.
     * @param t Throwable to be logged
     */
    void logException(String msg, Throwable t) {
        if (LOG_INFO) {
            System.out.println("** " + threadID() + ": " + msg);
            t.printStackTrace();
        }
    }


    /**
     * Create a printable representation of this AppProxy.
     * @return a printable string
     */
    public String toString() {
        if (LOG_INFO) {
            return"class: " + classname +
                ", suite: " + storageId +
                ", registered: " + isRegistered +
                ", name: " + applicationName +
                ", ID: " + applicationID;
        } else {
            return super.toString();
        }
    }

    /**
     * Map a thread to an printable string.
     * @return a short string for the thread
     */
    private String threadID() {
        if (LOG_INFO) {
            Thread thread = Thread.currentThread();
            int i = thread.hashCode() & 0xff;
            return "T" + i;
        } else {
            return "";
        }
    }
}
