// Copyright (c) 2010 Shardul Deo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package com.googlecode.protobuf.socketrpc;

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.protobuf.BlockingRpcChannel;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.protobuf.RpcCallback;
import com.google.protobuf.RpcChannel;
import com.google.protobuf.RpcController;
import com.google.protobuf.ServiceException;
import com.google.protobuf.Descriptors.MethodDescriptor;
import com.googlecode.protobuf.socketrpc.RpcConnectionFactory.Connection;
import com.googlecode.protobuf.socketrpc.SocketRpcProtos.ErrorReason;
import com.googlecode.protobuf.socketrpc.SocketRpcProtos.Response;

/**
 * {@link RpcChannel} implementation that uses a {@link RpcConnectionFactory} to
 * perform blocking and non-blocking rpcs.
 *
 * @author Shardul Deo
 */
class RpcChannelImpl implements RpcChannel, BlockingRpcChannel {

  private final static Logger LOG =
      Logger.getLogger(RpcChannelImpl.class.getName());

  private final RpcConnectionFactory connectionFactory;
  private final Executor executor;

  RpcChannelImpl(RpcConnectionFactory connectionFactory, Executor executor) {
    this.connectionFactory = connectionFactory;
    this.executor = executor;
  }

  @Override
  public void callMethod(MethodDescriptor method, RpcController controller,
      Message request, final Message responsePrototype,
      final RpcCallback<Message> done) {
    // Must pass in a SocketRpcController
    final SocketRpcController socketController
        = (SocketRpcController) controller;

    // Send request over connection
    final Connection connection;
    try {
      connection = createConnection(socketController);
    } catch (ServiceException e) {
      // Call done with null, controller has the error information
      callbackWithNull(done);
      return;
    }

    try {
      sendRpcRequest(method, socketController, request, connection);
    } catch (ServiceException e) {
      // Call done with null, controller has the error information
      try {
        callbackWithNull(done);
      } finally {
        close(connection);
      }
      return;
    }

    // Listen for the response using the executor
    executor.execute(new Runnable() {
      @Override
      public void run() {
        try {
          // Thread blocks here until server sends a response
          Response rpcResponse = receiveRpcResponse(socketController,
              connection);
          Message response = handleRpcResponse(responsePrototype, rpcResponse,
              socketController);

          // Callback if failed or server invoked callback
          if (socketController.failed() || rpcResponse.getCallback()) {
            if (done != null) {
              done.run(response);
            }
          }
        } catch (ServiceException e) {
          // Call done with null, controller has the error information
          callbackWithNull(done);
        } finally {
          close(connection);
        }
      }
    });
  }

  private static void callbackWithNull(RpcCallback<Message> done) {
    if (done != null) {
      done.run(null);
    }
  }

  @Override
  public Message callBlockingMethod(MethodDescriptor method,
      RpcController controller, Message request, Message responsePrototype)
      throws ServiceException {
    // Must pass in a SocketRpcController
    SocketRpcController socketController = (SocketRpcController) controller;
    final Connection connection = createConnection(socketController);
    try {
      sendRpcRequest(method, socketController, request, connection);
      Response rpcResponse = receiveRpcResponse(socketController, connection);
      return handleRpcResponse(responsePrototype, rpcResponse,
          socketController);
    } finally {
      close(connection);
    }
  }

  private Connection createConnection(SocketRpcController socketController)
      throws ServiceException {
    try {
      return connectionFactory.createConnection();
    } catch (UnknownHostException e) {
      return handleError(socketController, ErrorReason.UNKNOWN_HOST,
          "Could not find host: " + e.getMessage(), e);
    } catch (IOException e) {
      return handleError(socketController, ErrorReason.IO_ERROR, String.format(
          "Error creating connection using factory %s", connectionFactory), e);
    }
  }

  private void close(Connection connection) {
    try {
      connection.close();
    } catch (IOException e) {
      // It's ok
    }
  }

  private void sendRpcRequest(MethodDescriptor method,
      SocketRpcController socketController, Message request,
      Connection connection) throws ServiceException {
    // Check request
    if (!request.isInitialized()) {
      handleError(socketController, ErrorReason.INVALID_REQUEST_PROTO,
          "Request is uninitialized", null);
    }

    // Create RPC request protobuf
    SocketRpcProtos.Request rpcRequest = SocketRpcProtos.Request.newBuilder()
        .setRequestProto(request.toByteString())
        .setServiceName(method.getService().getFullName())
        .setMethodName(method.getName())
        .build();

    // Send request
    try {
      connection.sendProtoMessage(rpcRequest);
    } catch (IOException e) {
      handleError(socketController, ErrorReason.IO_ERROR, String.format(
          "Error writing over connection %s", connection), e);
    }
  }

  private Response receiveRpcResponse(SocketRpcController socketController,
      Connection connection) throws ServiceException {
    try {
      // Read and handle response
      SocketRpcProtos.Response.Builder builder = SocketRpcProtos.Response
          .newBuilder();
      connection.receiveProtoMessage(builder);
      if (!builder.isInitialized()) {
        return handleError(socketController, ErrorReason.BAD_RESPONSE_PROTO,
            "Bad response from server", null);
      }
      return builder.build();
    } catch (IOException e) {
      return handleError(socketController, ErrorReason.IO_ERROR, String.format(
          "Error reading over connection %s", connection), e);
    }
  }

  private Message handleRpcResponse(Message responsePrototype,
      SocketRpcProtos.Response rpcResponse,
      SocketRpcController socketController)
      throws ServiceException {

    // Check for error
    if (rpcResponse.hasError()) {
      return handleError(socketController, rpcResponse.getErrorReason(),
          rpcResponse.getError(), null);
    }

    if (!rpcResponse.hasResponseProto()) {
      // No response
      return null;
    }

    try {
      Message.Builder builder = responsePrototype.newBuilderForType()
          .mergeFrom(rpcResponse.getResponseProto());
      if (!builder.isInitialized()) {
        return handleError(socketController, ErrorReason.BAD_RESPONSE_PROTO,
            "Uninitialized RPC Response Proto", null);
      }
      return builder.build();
    } catch (InvalidProtocolBufferException e) {
      return handleError(socketController, ErrorReason.BAD_RESPONSE_PROTO,
          "Response could be parsed as "
              + responsePrototype.getClass().getName(), e);
    }
  }

  private <T> T handleError(SocketRpcController socketController,
      ErrorReason reason, String msg, Exception e)
      throws ServiceException {
    if (e == null) {
      LOG.log(Level.WARNING, reason + ": " + msg);
    } else {
      LOG.log(Level.WARNING, reason + ": " + msg, e);
    }
    socketController.setFailed(msg, reason);
    throw new ServiceException(msg);
  }
}