/*
 * Copyright 2009 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.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
import com.gargoylesoftware.htmlunit.javascript.host.Window;

import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;

import java.io.IOException;
import java.util.Collections;

/**
 * HTMLUnit object that represents the hosted-mode plugin.
 */
public class HostedModePluginObject extends ScriptableObject {

  /**
   * Function object which implements the connect method on the hosted-mode
   * plugin.
   */
  private class ConnectMethod extends ScriptableObject implements Function {

    private static final long serialVersionUID = -8799481412144205779L;
    private static final int EXPECTED_NUM_ARGS = 5;

    @Override
    public Object call(Context context, Scriptable scope, Scriptable thisObj,
        Object[] args) {
      // Allow extra arguments for forward compatibility
      if (args.length < EXPECTED_NUM_ARGS) {
        throw Context.reportRuntimeError("Bad number of parameters for function"
            + " connect: expected "
            + EXPECTED_NUM_ARGS
            + ", got "
            + args.length);
      }
      try {
        /*
         * connect arguments: url, sessionKey, ipAddress:port, moduleName,
         * hostedHtmlVersion
         */
        return connect((String) args[0], (String) args[1], (String) args[2],
            (String) args[3], (String) args[4]);
      } catch (ClassCastException e) {
        throw Context.reportRuntimeError("Incorrect parameter types for "
            + " connect: expected String/String/String/String/String");
      }
    }

    @Override
    public Scriptable construct(Context context, Scriptable scope, Object[] args) {
      throw Context.reportRuntimeError("Function connect can't be used as a "
          + "constructor");
    }

    @Override
    public String getClassName() {
      return "function HostedModePluginObject.connect";
    }
  }

  /**
   * Function object which implements the disconnect method on the hosted-mode
   * plugin.
   */
  private class DisconnectMethod extends ScriptableObject implements Function {

    private static final long serialVersionUID = -8799481412144519779L;

    @Override
    public Object call(Context context, Scriptable scope, Scriptable thisObj,
        Object[] args) {
      // Allow extra arguments for forward compatibility
      return disconnect();
    }

    @Override
    public Scriptable construct(Context context, Scriptable scope, Object[] args) {
      throw Context.reportRuntimeError("Function disconnect can't be used as a "
          + "constructor");
    }

    @Override
    public String getClassName() {
      return "function HostedModePluginObject.disconnect";
    }
  }

  /**
   * Function object which implements the init method on the hosted-mode plugin.
   */
  private class InitMethod extends ScriptableObject implements Function {

    private static final long serialVersionUID = -8799481412144205779L;
    private static final String VERSION = "2.0";

    @Override
    public Object call(Context context, Scriptable scope, Scriptable thisObj,
        Object[] args) {
      // Allow extra arguments for forward compatibility
      if (args.length < 1) {
        throw Context.reportRuntimeError("Bad number of parameters for function"
            + " init: expected 1, got " + args.length);
      }
      try {
        window = (Window) args[0];
        return init(VERSION);
      } catch (ClassCastException e) {
        throw Context.reportRuntimeError("Incorrect parameter types for init: expected Window");
      }
    }

    @Override
    public Scriptable construct(Context context, Scriptable scope, Object[] args) {
      throw Context.reportRuntimeError("Function init can't be used as a "
          + "constructor");
    }

    @Override
    public String getClassName() {
      return "function HostedModePluginObject.init";
    }
  }

  private static final long serialVersionUID = -1815031145376726799L;

  private Scriptable connectMethod;
  private Scriptable disconnectMethod;
  private Scriptable initMethod;
  private Window window;
  private final JavaScriptEngine jsEngine;
  private final WebClient webClient;
  private final TreeLogger logger;

  private BrowserChannelClient browserChannelClient;

  /**
   * Creates a HostedModePluginObject with the passed-in JavaScriptEngine.
   *
   * @param jsEngine The JavaScriptEngine.
   * @param webClient The WebClient being tested.
   * @param logger A logger instance to notify the user of errors.
   */
  public HostedModePluginObject(JavaScriptEngine jsEngine, WebClient webClient, TreeLogger logger) {
    this.jsEngine = jsEngine;
    this.webClient = webClient;
    this.logger = logger;
  }

  /**
   * Initiate a hosted mode connection to the requested port and load the
   * requested module.
   *
   * @param url the complete url
   * @param sessionKey a length 16 string to identify a "session"
   * @param address "host:port" or "ipAddress:port" to use for the OOPHM server
   * @param module module name to load
   * @param version version string
   * @return true if the connection succeeds
   */
  public boolean connect(String url, String sessionKey, String address,
      String module, String version) {
    String addressParts[] = address.split(":");
    if (addressParts.length < 2) {
      logger.log(TreeLogger.ERROR, "connect failed because address " + address
          + " was not of the form foo.com:8080");
      return false;
    }
    // TODO: add whitelist and default-port support?

    // We know that legacy dev mode is running, we need to tell HtmlUnit that it is safe
    // to permit plain Java objects to leak into JS - the JavaObject type will return a
    // Object[] with a success boolean and a value, and HtmlUnit will guard against this.
    // The simplest way to do that here is to mark java.lang.Object as the java equivalent
    // of some JS type - the name of the type doesn't matter.
    webClient.setActiveXObjectMap(Collections.singletonMap(
            "GwtLegacyDevModeExceptionOrReturnValue", "java.lang.Object"));

    try {
      HtmlUnitSessionHandler htmlUnitSessionHandler = new HtmlUnitSessionHandler(
          window, jsEngine, webClient);
      browserChannelClient = new BrowserChannelClient(addressParts, url,
          sessionKey, module, version, htmlUnitSessionHandler);
      htmlUnitSessionHandler.setSessionData(new SessionData(
          htmlUnitSessionHandler, browserChannelClient));
      return browserChannelClient.process();
    } catch (BrowserChannelException e) {
      logger.log(TreeLogger.ERROR,
          "BrowserChannelException returned from connect " + e.getMessage(), e);
      return false;
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, "IOException returned from connect "
          + e.getMessage(), e);
      return false;
    }
  }

  public boolean disconnect() {
    try {
      return browserChannelClient.disconnectFromHost();
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, "IOException returned from disconnect "
          + e.getMessage(), e);
      return false;
    }
  }

  @Override
  public Object get(String name, Scriptable start) {
    if ("connect".equals(name)) {
      if (connectMethod == null) {
        connectMethod = new ConnectMethod();
      }
      return connectMethod;
    } else if ("disconnect".equals(name)) {
      if (disconnectMethod == null) {
        disconnectMethod = new DisconnectMethod();
      }
      return disconnectMethod;
    } else if ("init".equals(name)) {
      if (initMethod == null) {
        initMethod = new InitMethod();
      }
      return initMethod;
    }
    return NOT_FOUND;
  }

  @Override
  public String getClassName() {
    return "HostedModePluginObject";
  }

  /**
   * Verify that the plugin can be initialized properly and supports the
   * requested version.
   *
   * @param version hosted mode protocol version
   * @return true if initialization succeeds, otherwise false
   */
  public boolean init(String version) {
    // TODO: what needs to be done here?
    return true;
  }
}
