/*
 * Copyright 2008 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;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.ModuleHandle;
import com.google.gwt.dev.shell.BrowserChannel.Value;
import com.google.gwt.dev.shell.BrowserChannelServer.SessionHandlerServer;
import com.google.gwt.dev.shell.JsValue.DispatchMethod;
import com.google.gwt.dev.shell.JsValue.DispatchObject;
import com.google.gwt.dev.util.log.speedtracer.DevModeEventType;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;

import java.lang.reflect.Member;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 *
 */
public class OophmSessionHandler extends SessionHandlerServer {

  private BrowserWidgetHost host;

  private Map<BrowserChannelServer, ModuleSpace> moduleMap = Collections.synchronizedMap(new HashMap<BrowserChannelServer, ModuleSpace>());

  private Map<BrowserChannelServer, ModuleHandle> moduleHandleMap = Collections.synchronizedMap(new HashMap<BrowserChannelServer, ModuleHandle>());

  private final TreeLogger topLogger;

  /**
   * Listens for new connections from browsers.
   *
   * @param topLogger logger to use for non-module-related messages
   * @param host BrowserWidgetHost instance
   */
  public OophmSessionHandler(TreeLogger topLogger, BrowserWidgetHost host) {
    this.host = host;
    this.topLogger = topLogger;
  }

  @Override
  public void freeValue(BrowserChannelServer channel, int[] ids) {
    ServerObjectsTable localObjects = channel.getJavaObjectsExposedInBrowser();
    for (int id : ids) {
      localObjects.free(id);
    }
  }

  @Override
  public ExceptionOrReturnValue getProperty(BrowserChannelServer channel,
      int refId, int dispId) {
    ModuleSpace moduleSpace = moduleMap.get(channel);
    ModuleHandle moduleHandle = moduleHandleMap.get(channel);
    assert moduleSpace != null && moduleHandle != null;
    TreeLogger logger = moduleHandle.getLogger();
    ServerObjectsTable localObjects = channel.getJavaObjectsExposedInBrowser();
    try {
      JsValueOOPHM obj = new JsValueOOPHM();
      DispatchObject dispObj;
      CompilingClassLoader ccl = moduleSpace.getIsolatedClassLoader();
      obj.setWrappedJavaObject(ccl, localObjects.get(refId));
      dispObj = obj.getJavaObjectWrapper();
      TreeLogger branch = logger.branch(TreeLogger.SPAM,
          "Client special invoke of getProperty(" + dispId + " ["
              + ccl.getClassInfoByDispId(dispId).getMember(dispId) + "]) on "
              + obj.toString(), null);
      JsValueOOPHM jsval = (JsValueOOPHM) dispObj.getField(dispId);
      Value retVal = channel.convertFromJsValue(localObjects, jsval);
      if (logger.isLoggable(TreeLogger.SPAM)) {
        branch.log(TreeLogger.SPAM, "result is " + retVal, null);
      }
      return new ExceptionOrReturnValue(false, retVal);
    } catch (Throwable t) {
      JsValueOOPHM jsval = new JsValueOOPHM();
      JsValueGlue.set(jsval, moduleSpace.getIsolatedClassLoader(),
          t.getClass(), t);
      Value retVal = channel.convertFromJsValue(localObjects, jsval);
      return new ExceptionOrReturnValue(true, retVal);
    }
  }

  /**
   * Invoke a method on a server object in from client code.
   */
  @Override
  public ExceptionOrReturnValue invoke(BrowserChannelServer channel,
      Value thisVal, int methodDispatchId, Value[] args) {
    Event jsToJavaCallEvent =
        SpeedTracerLogger.start(channel.getDevModeSession(), DevModeEventType.JS_TO_JAVA_CALL);
    ServerObjectsTable localObjects = channel.getJavaObjectsExposedInBrowser();
    ModuleSpace moduleSpace = moduleMap.get(channel);
    ModuleHandle moduleHandle = moduleHandleMap.get(channel);
    assert moduleSpace != null && moduleHandle != null;
    TreeLogger logger = moduleHandle.getLogger();
    CompilingClassLoader cl = moduleSpace.getIsolatedClassLoader();

    // Treat dispatch id 0 as toString()
    if (methodDispatchId == 0) {
      methodDispatchId = cl.getDispId("java.lang.Object::toString()");
    }

    JsValueOOPHM jsThis = new JsValueOOPHM();
    channel.convertToJsValue(cl, localObjects, thisVal, jsThis);

    if (SpeedTracerLogger.jsniCallLoggingEnabled()) {
      DispatchClassInfo clsInfo = cl.getClassInfoByDispId(methodDispatchId);
      if (clsInfo != null) {
        Member member = clsInfo.getMember(methodDispatchId);
        if (member != null) {
          jsToJavaCallEvent.addData("name", member.toString());
        }
      }
    }

    TreeLogger branch = TreeLogger.NULL;
    if (logger.isLoggable(TreeLogger.SPAM)) {
      StringBuffer logMsg = new StringBuffer();
      logMsg.append("Client invoke of ");
      logMsg.append(methodDispatchId);
      DispatchClassInfo classInfo = cl.getClassInfoByDispId(methodDispatchId);
      if (classInfo != null) {
        Member member = classInfo.getMember(methodDispatchId);
        if (member != null) {
          logMsg.append(" (");
          logMsg.append(member.getName());
          logMsg.append(")");
        }
      }
      logMsg.append(" on ");
      logMsg.append(jsThis.toString());
      branch = logger.branch(TreeLogger.SPAM, logMsg.toString(), null);
    }
    JsValueOOPHM[] jsArgs = new JsValueOOPHM[args.length];
    for (int i = 0; i < args.length; ++i) {
      jsArgs[i] = new JsValueOOPHM();
      channel.convertToJsValue(cl, localObjects, args[i], jsArgs[i]);
      if (logger.isLoggable(TreeLogger.SPAM)) {
        branch.log(TreeLogger.SPAM, " arg " + i + " = " + jsArgs[i].toString(),
            null);
      }
    }
    JsValueOOPHM jsRetVal = new JsValueOOPHM();
    JsValueOOPHM jsMethod;
    DispatchObject dispObj;
    if (jsThis.isWrappedJavaObject()) {
      // If this is a wrapped object, get get the method off it.
      dispObj = jsThis.getJavaObjectWrapper();
    } else {
      // Look it up on the static dispatcher.
      dispObj = (DispatchObject) moduleSpace.getStaticDispatcher();
    }
    jsMethod = (JsValueOOPHM) dispObj.getField(methodDispatchId);
    DispatchMethod dispMethod = jsMethod.getWrappedJavaFunction();
    boolean exception;
    try {
      exception = dispMethod.invoke(jsThis, jsArgs, jsRetVal);
    } catch (Throwable t) {
      exception = true;
      JsValueGlue.set(jsRetVal, moduleSpace.getIsolatedClassLoader(),
          t.getClass(), t);
    }
    Value retVal = channel.convertFromJsValue(localObjects, jsRetVal);
    jsToJavaCallEvent.end();
    return new ExceptionOrReturnValue(exception, retVal);
  }

  @Override
  public synchronized TreeLogger loadModule(BrowserChannelServer channel,
      String moduleName, String userAgent, String url, String tabKey,
      String sessionKey, byte[] userAgentIcon) {
    Event moduleInit =
        SpeedTracerLogger.start(channel.getDevModeSession(), DevModeEventType.MODULE_INIT,
            "Module Name", moduleName);
    ModuleHandle moduleHandle = host.createModuleLogger(moduleName, userAgent,
        url, tabKey, sessionKey, channel, userAgentIcon);
    TreeLogger logger = moduleHandle.getLogger();
    moduleHandleMap.put(channel, moduleHandle);
    ModuleSpace moduleSpace = null;
    try {
      // Attach a new ModuleSpace to make it programmable.
      ModuleSpaceHost msh = host.createModuleSpaceHost(moduleHandle, moduleName);
      moduleSpace = new ModuleSpaceOOPHM(msh, moduleName, channel);
      moduleMap.put(channel, moduleSpace);
      moduleSpace.onLoad(logger);
      if (logger.isLoggable(TreeLogger.INFO)) {
        moduleHandle.getLogger().log(TreeLogger.INFO,
            "Module " + moduleName + " has been loaded");
      }
    } catch (Throwable e) {
      // We do catch Throwable intentionally because there are a ton of things
      // that can go wrong trying to load a module, including Error-derived
      // things like NoClassDefFoundError.
      //
      moduleHandle.getLogger().log(
          TreeLogger.ERROR,
          "Failed to load module '" + moduleName + "' from user agent '"
              + userAgent + "' at " + channel.getRemoteEndpoint(), e);
      if (moduleSpace != null) {
        moduleSpace.dispose();
      }
      moduleHandle.unload();
      moduleMap.remove(channel);
      moduleHandleMap.remove(channel);
      return null;
    } finally {
      moduleInit.end();
    }
    return moduleHandle.getLogger();
  }

  @Override
  public ExceptionOrReturnValue setProperty(BrowserChannelServer channel,
      int refId, int dispId, Value newValue) {
    ModuleSpace moduleSpace = moduleMap.get(channel);
    ModuleHandle moduleHandle = moduleHandleMap.get(channel);
    assert moduleSpace != null && moduleHandle != null;
    TreeLogger logger = moduleHandle.getLogger();
    ServerObjectsTable localObjects = channel.getJavaObjectsExposedInBrowser();
    try {
      JsValueOOPHM obj = new JsValueOOPHM();
      DispatchObject dispObj;
      obj.setWrappedJavaObject(moduleSpace.getIsolatedClassLoader(),
          localObjects.get(refId));
      dispObj = obj.getJavaObjectWrapper();
      if (logger.isLoggable(TreeLogger.SPAM)) {
        logger.log(TreeLogger.SPAM, "Client special invoke of setProperty(id="
            + dispId + ", newValue=" + newValue + ") on " + obj.toString(), null);
      }
      JsValueOOPHM jsval = new JsValueOOPHM();
      channel.convertToJsValue(moduleSpace.getIsolatedClassLoader(),
          localObjects, newValue, jsval);
      dispObj.setField(dispId, jsval);
      return new ExceptionOrReturnValue(false, newValue);
    } catch (Throwable t) {
      JsValueOOPHM jsval = new JsValueOOPHM();
      JsValueGlue.set(jsval, moduleSpace.getIsolatedClassLoader(),
          t.getClass(), t);
      Value retVal = channel.convertFromJsValue(localObjects, jsval);
      return new ExceptionOrReturnValue(true, retVal);
    }
  }

  @Override
  public void unloadModule(BrowserChannelServer channel, String moduleName) {
    ModuleHandle moduleHandle = moduleHandleMap.get(channel);
    ModuleSpace moduleSpace = moduleMap.get(channel);
    if (moduleSpace == null || moduleHandle == null) {
      topLogger.log(TreeLogger.ERROR, "Unload request without a module loaded",
          null);
      return;
    }
    moduleHandle.getLogger().log(
        TreeLogger.INFO,
        "Unloading module " + moduleSpace.getModuleName() + " (" + moduleName
            + ")", null);
    moduleSpace.dispose();
    moduleHandle.unload();
    moduleMap.remove(channel);
    moduleHandleMap.remove(channel);
  }
}
