/*
 * Copyright 2007 Google Inc.
 * 
 * Licensed 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 com.google.gwt.dev.shell.moz;

import com.google.gwt.dev.shell.LowLevel;

import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Vector;

/**
 * Various low-level helper methods for dealing with Gecko.
 */
public class LowLevelMoz {

  /**
   * Provides interface for methods to be exposed on JavaScript side.
   */
  public interface DispatchMethod {
    /**
     * Invoke a Java method from JavaScript.
     * 
     * @param jsthis the wrapped Java object to invoke
     * @param jsargs an array of JavaScript values to pass as parameters
     * @param returnValue the JavaScript value in which to store the returned
     *          value
     */
    void invoke(int jsthis, int[] jsargs, int returnValue);
  }

  /**
   * Provides interface for objects to be exposed to JavaScript code.
   */
  public interface DispatchObject {
    /**
     * Retrieve a field from an object.
     * 
     * @param name the name of the field
     * @param value pointer to the JsRootedValue to receive the field value
     */
    void getField(String name, int value);

    Object getTarget();

    /**
     * Set the value of a field on an object.
     * 
     * @param name the name of the field
     * @param value pointer to the JsRootedValue to store into the field
     */
    void setField(String name, int value);
  }

  interface ExternalFactory {
    ExternalObject createExternalObject();

    boolean matchesDOMWindow(int domWindow);
  }

  /**
   * TODO: rip this whole thing out if possible and use DispatchObject like on
   * Safari.
   */
  interface ExternalObject {
    boolean initModule(String moduleName);

    boolean gwtOnLoad(int scriptGlobalObject, String moduleName, String version);
  }

  /**
   * Stores a map from DispatchObject/DispatchMethod to the live underlying
   * jsval. This is used to both preserve identity for the same Java Object and
   * also prevent GC.
   */
  static Map<Object, Integer> sObjectToJsval = new IdentityHashMap<Object, Integer>();

  private static Vector<ExternalFactory> sExternalFactories = new Vector<ExternalFactory>();
  private static boolean sInitialized = false;

  /**
   * Executes JavaScript code, retaining file and line information.
   * 
   * @param scriptObject An opaque handle to the script frame window
   * @param code The JavaScript code to execute
   * @param file A file name associated with the code
   * @param line A line number associated with the code.
   */
  public static void executeScriptWithInfo(int scriptObject, String code,
      String file, int line) {
    if (!_executeScriptWithInfo(scriptObject, code, file, line)) {
      throw new RuntimeException(file + "(" + line
          + "): Failed to execute script: " + code);
    }
  }

  public static synchronized void init() {
    // Force LowLevel initialization to load gwt-ll
    LowLevel.init();
    if (!sInitialized) {
      if (!_registerExternalFactoryHandler()) {
        throw new RuntimeException(
            "Failed to register external factory handler.");
      }
      sInitialized = true;
    }
  }

  /**
   * Invokes a method implemented in JavaScript.
   * 
   * @param scriptObject An opaque handle to the script frame window
   * @param methodName the method name on jsthis to call
   * @param jsthis A wrapped java object as a JsRootedValue pointer
   * @param jsargs the arguments to pass to the method as JsRootedValue pointers
   * @param retval the jsvalue to write the result into
   * @throws RuntimeException if the invoke fails
   */
  public static void invoke(int scriptObject, String methodName, int jsthis,
      int[] jsargs, int retval) {
    if (!_invoke(scriptObject, methodName, jsthis, jsargs, retval)) {
      throw new RuntimeException("Failed to invoke native method: "
          + methodName + " with " + jsargs.length + " arguments.");
    }
  }

  /**
   * Call this to raise an exception in JavaScript before returning control.
   * Currently, the JavaScript exception throw is always null.
   */
  public static void raiseJavaScriptException() {
    if (!_raiseJavaScriptException()) {
      throw new RuntimeException(
          "Failed to raise Java Exception into JavaScript.");
    }
  }

  /**
   * BrowserWindows register here so that if their contained window gets a call
   * to window.external, the call can be routed correctly by nsIDOMWindow
   * pointer.
   * 
   * @param externalFactory the factory to register
   */
  public static void registerExternalFactory(ExternalFactory externalFactory) {
    synchronized (sExternalFactories) {
      sExternalFactories.add(externalFactory);
    }
  }

  /**
   * Unregisters an existing registration.
   * 
   * @param externalFactory the factory to unregister
   */
  public static void unregisterExternalFactory(ExternalFactory externalFactory) {
    synchronized (sExternalFactories) {
      sExternalFactories.remove(externalFactory);
    }
  }

  /**
   * Called from native code to create an external object for a particular
   * window.
   * 
   * @param domWindow an nsIDOMWindow to check against our ExternalFactories map
   * @return a new ExternalObject
   */
  protected static ExternalObject createExternalObjectForDOMWindow(int domWindow) {
    for (ExternalFactory fac : sExternalFactories) {
      if (fac.matchesDOMWindow(domWindow)) {
        return fac.createExternalObject();
      }
    }
    return null;
  }

  /**
   * Called from native code to do tracing.
   * 
   * @param s the string to trace
   */
  protected static void trace(String s) {
    System.out.println(s);
    System.out.flush();
  }

  /**
   * Native code accessor to remove the mapping upon GC.
   */
  static void removeJsvalForObject(Object o) {
    sObjectToJsval.remove(o);
  }

  // CHECKSTYLE_NAMING_OFF: Non JSNI native code may have leading '_'s.

  private static native boolean _executeScriptWithInfo(int scriptObject,
      String newScript, String file, int line);

  /**
   * Native method for invoking a JavaScript method.
   * 
   * @param scriptObject nsIScriptGlobalObject* as an int
   * @param methodName name of JavaScript method
   * @param jsThisInt JavaScript object to invoke the method on, as a
   *          JsRootedValue int
   * @param jsArgsInt array of arguments, as an array of JsRootedValue ints
   * @param jsRetValint pointer to JsRootedValue to receive return value
   * @return true on success
   */
  private static native boolean _invoke(int scriptObject, String methodName,
      int jsThisInt, int[] jsArgsInt, int jsRetValInt);

  private static native boolean _raiseJavaScriptException();

  private static native boolean _registerExternalFactoryHandler();

  // CHECKSTYLE_NAMING_ON

  /**
   * Print debug information for a JS method invocation.
   * 
   * TODO(jat): remove this method
   * 
   * @param methodName the name of the JS method being invoked
   * @param jsthis the JS object with the named method
   * @param jsargs an array of arguments to the method
   */
  @SuppressWarnings("unused")
  // kept for future debugging purposes
  private static void printInvocationParams(String methodName,
      JsValueMoz jsthis, JsValueMoz[] jsargs) {
    System.out.println("LowLevelMoz.invoke:");
    System.out.println(" method = " + methodName);
    System.out.println(" # args = " + (jsargs.length));
    System.out.println(" jsthis = " + jsthis.toString());
    for (int i = 0; i < jsargs.length; ++i) {
      System.out.println(" jsarg[" + i + "] = " + jsargs[i].toString());
    }
    System.out.println("");
  }

  /**
   * Not instantiable.
   */
  private LowLevelMoz() {
  }
}
