/*
 * 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.google.gwt.dev.shell.BrowserChannel.SessionHandler.ExceptionOrReturnValue;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;

import java.io.IOException;
import java.net.Socket;
import java.util.Arrays;
import java.util.Set;

/**
 * Implementation of the BrowserChannel for the client side.
 */
public class BrowserChannelClient extends BrowserChannel {

  /**
   * Hook interface for responding to messages from the server.
   */
  public abstract static class SessionHandlerClient extends
      SessionHandler<BrowserChannelClient> {

    public abstract Object getSynchronizationObject();

    public abstract String getUserAgent();

    public abstract ExceptionOrReturnValue invoke(BrowserChannelClient channel,
        Value thisObj, String methodName, Value[] args);

    public abstract void loadJsni(BrowserChannelClient channel,
        String jsniString);
  }

  private static class ClientObjectRefFactory implements ObjectRefFactory {

    private final RemoteObjectTable<JavaObjectRef> remoteObjectTable;

    public ClientObjectRefFactory() {
      remoteObjectTable = new RemoteObjectTable<JavaObjectRef>();
    }

    @Override
    public JavaObjectRef getJavaObjectRef(int refId) {
      JavaObjectRef objectRef = remoteObjectTable.getRemoteObjectRef(refId);
      if (objectRef == null) {
        objectRef = new JavaObjectRef(refId);
        remoteObjectTable.putRemoteObjectRef(refId, objectRef);
      }
      return objectRef;
    }

    @Override
    public JsObjectRef getJsObjectRef(int refId) {
      return new JsObjectRef(refId);
    }

    @Override
    public Set<Integer> getRefIdsForCleanup() {
      return remoteObjectTable.getRefIdsForCleanup();
    }
  }

  private final SessionHandlerClient handler;
  private final PrintWriterTreeLogger logger = new PrintWriterTreeLogger();
  private final String moduleName;
  private final String tabKey;
  private final String sessionKey;
  private final String url;
  private final String versionString;
  private boolean connected = false;
  private int protocolVersion;

  public BrowserChannelClient(String addressParts[], String url,
      String sessionKey, String moduleName, String versionString,
      SessionHandlerClient sessionHandlerClient) throws IOException {
    super(new Socket(addressParts[0], Integer.parseInt(addressParts[1])),
        new ClientObjectRefFactory());
    connected = true;
    this.url = url;
    this.sessionKey = sessionKey;
    this.moduleName = moduleName;
    this.tabKey = ""; // TODO(jat): update when tab support is added.
    this.versionString = versionString;
    logger.setMaxDetail(TreeLogger.WARN);
    if (logger.isLoggable(TreeLogger.SPAM)) {
      logger.log(TreeLogger.SPAM, "BrowserChannelClient, versionString: "
          + versionString);
    }
    this.handler = sessionHandlerClient;
  }

  public boolean disconnectFromHost() throws IOException {
    if (logger.isLoggable(TreeLogger.DEBUG)) {
      logger.log(TreeLogger.DEBUG, "disconnecting channel " + this);
    }
    if (!isConnected()) {
      if (logger.isLoggable(TreeLogger.DEBUG)) {
        logger.log(TreeLogger.DEBUG,
            "Disconnecting already disconnected channel " + this);
      }
      return false;
    }
    new QuitMessage(this).send();
    endSession();
    connected = false;
    return true;
  }

  /**
   * @return the negotiated protocol version -- only valid after {@link #init}
   *         has returned.
   */
  public int getProtocolVersion() {
    return protocolVersion;
  }

  public boolean isConnected() {
    return connected;
  }

  // TODO (amitmanjhi): refer the state (message?) transition diagram
  /**
   * returns true iff execution completes normally.
   */
  public boolean process() throws IOException, BrowserChannelException {
    if (!init()) {
      disconnectFromHost();
      return false;
    }
    if (logger.isLoggable(TreeLogger.DEBUG)) {
      logger.log(TreeLogger.DEBUG, "sending " + MessageType.LOAD_MODULE
          + " message, userAgent: " + handler.getUserAgent());
    }
    ReturnMessage returnMessage = null;
    synchronized (handler.getSynchronizationObject()) {
      new LoadModuleMessage(this, url, tabKey, sessionKey, moduleName,
          handler.getUserAgent()).send();
      returnMessage = reactToMessages(handler, true);
    }
    if (logger.isLoggable(TreeLogger.DEBUG)) {
      logger.log(TreeLogger.DEBUG, "loaded module, returnValue: "
          + returnMessage.getReturnValue() + ", isException: "
          + returnMessage.isException());
    }
    return !returnMessage.isException();
  }

  public ReturnMessage reactToMessagesWhileWaitingForReturn(
      SessionHandlerClient handler) throws IOException, BrowserChannelException {
    ReturnMessage returnMessage = reactToMessages(handler, true);
    return returnMessage;
  }

  /*
   * Perform the initial interaction. Return true if interaction succeeds, false
   * if it fails. Do a check protocol versions, expected with 2.0+ oophm
   * protocol.
   */
  private boolean init() throws IOException, BrowserChannelException {
    if (logger.isLoggable(TreeLogger.DEBUG)) {
      logger.log(TreeLogger.DEBUG, "sending " + MessageType.CHECK_VERSIONS
          + " message");
    }
    new CheckVersionsMessage(this, PROTOCOL_VERSION_OLDEST,
        PROTOCOL_VERSION_CURRENT, versionString).send();
    MessageType type = Message.readMessageType(getStreamFromOtherSide());
    switch (type) {
      case PROTOCOL_VERSION:
        ProtocolVersionMessage protocolMessage = ProtocolVersionMessage.receive(this);
        protocolVersion = protocolMessage.getProtocolVersion();
        if (logger.isLoggable(TreeLogger.DEBUG)) {
          logger.log(TreeLogger.DEBUG, MessageType.PROTOCOL_VERSION
              + ": protocol version = " + protocolVersion);
        }
        break;
      case FATAL_ERROR:
        FatalErrorMessage errorMessage = FatalErrorMessage.receive(this);
        if (logger.isLoggable(TreeLogger.DEBUG)) {
          logger.log(TreeLogger.ERROR, "Received FATAL_ERROR message "
              + errorMessage.getError());
        }
        return false;
      default:
        return false;
    }

    return true;
  }

  private ReturnMessage reactToMessages(SessionHandlerClient handler,
      boolean expectReturn) throws IOException, BrowserChannelException {
    while (true) {
      ExceptionOrReturnValue returnValue;
      MessageType type = Message.readMessageType(getStreamFromOtherSide());
      if (logger.isLoggable(TreeLogger.INFO)) {
        logger.log(TreeLogger.INFO, "client: received " + type + ", thread: "
            + Thread.currentThread().getName());
      }
      try {
        switch (type) {
          case INVOKE:
            InvokeOnClientMessage invokeMessage = InvokeOnClientMessage.receive(this);
            returnValue = handler.invoke(this, invokeMessage.getThis(),
                invokeMessage.getMethodName(), invokeMessage.getArgs());
            new ReturnMessage(this, returnValue.isException(),
                returnValue.getReturnValue()).send();
            break;
          case FREE_VALUE:
            FreeMessage freeMessage = FreeMessage.receive(this);
            if (logger.isLoggable(TreeLogger.DEBUG)) {
              logger.log(TreeLogger.DEBUG,
                  type + " message " + Arrays.toString(freeMessage.getIds()));
            }
            handler.freeValue(this, freeMessage.getIds());
            // no response
            break;
          case LOAD_JSNI:
            LoadJsniMessage loadJsniMessage = LoadJsniMessage.receive(this);
            String jsniString = loadJsniMessage.getJsni();
            handler.loadJsni(this, jsniString);
            // no response
            break;
          case REQUEST_ICON:
            RequestIconMessage.receive(this);
            // no need for icon here
            UserAgentIconMessage.send(this, null);
            break;
          case RETURN:
            if (!expectReturn) {
              logger.log(TreeLogger.ERROR, "Received unexpected "
                  + MessageType.RETURN);
            }
            return ReturnMessage.receive(this);
          case QUIT:
            if (expectReturn) {
              logger.log(TreeLogger.ERROR, "Received " + MessageType.QUIT
                  + " while waiting for return");
            }
            disconnectFromHost();
            return null;
          default:
            logger.log(TreeLogger.ERROR, "Unkown messageType: " + type
                + ", expectReturn: " + expectReturn);
            disconnectFromHost();
            return null;
        }
      } catch (Exception ex) {
        logger.log(TreeLogger.ERROR, "Unknown exception" + ex);
        ex.printStackTrace();
      }
    }
  }
}
