/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2013-2015 ForgeRock AS
 */
package org.opends.server.protocols.http;

import static org.forgerock.opendj.adapter.server3x.Converters.*;
import static org.forgerock.opendj.ldap.ByteString.*;
import static org.forgerock.opendj.ldap.LdapException.*;
import static org.forgerock.opendj.ldap.spi.LdapPromiseImpl.*;

import java.util.LinkedHashSet;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.http.HttpServletResponse;

import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.AbstractAsynchronousConnection;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConnectionEventListener;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.LdapPromise;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.requests.AbandonRequest;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.CompareRequest;
import org.forgerock.opendj.ldap.requests.DeleteRequest;
import org.forgerock.opendj.ldap.requests.ExtendedRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
import org.forgerock.opendj.ldap.requests.UnbindRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.CompareResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.spi.LdapPromiseImpl;
import org.opends.server.core.AbandonOperation;
import org.opends.server.core.AbandonOperationBasis;
import org.opends.server.core.AddOperation;
import org.opends.server.core.AddOperationBasis;
import org.opends.server.core.BindOperation;
import org.opends.server.core.BindOperationBasis;
import org.opends.server.core.BoundedWorkQueueStrategy;
import org.opends.server.core.CompareOperation;
import org.opends.server.core.CompareOperationBasis;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.DeleteOperationBasis;
import org.opends.server.core.ExtendedOperation;
import org.opends.server.core.ExtendedOperationBasis;
import org.opends.server.core.ModifyDNOperation;
import org.opends.server.core.ModifyDNOperationBasis;
import org.opends.server.core.ModifyOperation;
import org.opends.server.core.ModifyOperationBasis;
import org.opends.server.core.QueueingStrategy;
import org.opends.server.core.SearchOperation;
import org.opends.server.core.SearchOperationBasis;
import org.opends.server.core.UnbindOperation;
import org.opends.server.core.UnbindOperationBasis;
import org.opends.server.protocols.ldap.AbandonRequestProtocolOp;
import org.opends.server.protocols.ldap.AddRequestProtocolOp;
import org.opends.server.protocols.ldap.BindRequestProtocolOp;
import org.opends.server.protocols.ldap.CompareRequestProtocolOp;
import org.opends.server.protocols.ldap.DeleteRequestProtocolOp;
import org.opends.server.protocols.ldap.ExtendedRequestProtocolOp;
import org.opends.server.protocols.ldap.LDAPMessage;
import org.opends.server.protocols.ldap.ModifyDNRequestProtocolOp;
import org.opends.server.protocols.ldap.ModifyRequestProtocolOp;
import org.opends.server.protocols.ldap.ProtocolOp;
import org.opends.server.protocols.ldap.SearchRequestProtocolOp;
import org.opends.server.protocols.ldap.UnbindRequestProtocolOp;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.DisconnectReason;
import org.opends.server.types.Operation;

/**
 * Adapter class between LDAP SDK's {@link org.forgerock.opendj.ldap.Connection}
 * and OpenDJ server's
 * {@link org.opends.server.protocols.http.HTTPClientConnection}.
 */
public class SdkConnectionAdapter extends AbstractAsynchronousConnection
{

  /** The tracer object for the debug logger. */
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** The HTTP client connection being "adapted". */
  private final HTTPClientConnection clientConnection;

  /**
   * The next message ID (and operation ID) that should be used for this
   * connection.
   */
  private final AtomicInteger nextMessageID = new AtomicInteger(0);

  /** The queueing strategy used for this connection. */
  private final QueueingStrategy queueingStrategy;

  /**
   * Whether this connection has been closed by calling {@link #close()} or
   * {@link #close(UnbindRequest, String)}.
   */
  private boolean isClosed;

  /**
   * Constructor.
   *
   * @param clientConnection
   *          the HTTP client connection being "adapted"
   */
  public SdkConnectionAdapter(HTTPClientConnection clientConnection)
  {
    this.clientConnection = clientConnection;
    this.queueingStrategy =
        new BoundedWorkQueueStrategy(clientConnection.getConnectionHandler()
            .getCurrentConfig().getMaxConcurrentOpsPerConnection());
  }

  private <R> LdapPromise<R> enqueueOperation(Operation operation)
  {
    return enqueueOperation(operation, null);
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  private <R> LdapPromise<R> enqueueOperation(Operation operation, SearchResultHandler entryHandler)
  {
    final LdapPromiseImpl<R> promise = newLdapPromiseImpl(operation.getMessageID());

    try
    {
      operation.setInnerOperation(this.clientConnection.isInnerConnection());

      HTTPConnectionHandler connHandler = this.clientConnection.getConnectionHandler();
      if (connHandler.keepStats())
      {
        connHandler.getStatTracker().updateMessageRead(
            new LDAPMessage(operation.getMessageID(), toRequestProtocolOp(operation)));
      }

      // need this raw cast here to fool the compiler's generic type safety
      // Problem here is due to the generic type R on enqueueOperation()
      clientConnection.addOperationInProgress(operation, (LdapPromiseImpl) promise, entryHandler);
      queueingStrategy.enqueueRequest(operation);
    }
    catch (Exception e)
    {
      logger.traceException(e);
      clientConnection.removeOperationInProgress(operation.getMessageID());
      // TODO JNR add error message??
      promise.handleException(newLdapException(ResultCode.OPERATIONS_ERROR, e));
    }

    return promise;
  }

  private ProtocolOp toRequestProtocolOp(Operation operation)
  {
    if (operation instanceof AbandonOperation)
    {
      final AbandonOperation op = (AbandonOperation) operation;
      return new AbandonRequestProtocolOp(op.getIDToAbandon());
    }
    else if (operation instanceof AddOperation)
    {
      final AddOperation op = (AddOperation) operation;
      return new AddRequestProtocolOp(op.getRawEntryDN(),
          op.getRawAttributes());
    }
    else if (operation instanceof BindOperation)
    {
      final BindOperation op = (BindOperation) operation;
      return new BindRequestProtocolOp(op.getRawBindDN(),
          op.getSASLMechanism(), op.getSASLCredentials());
    }
    else if (operation instanceof CompareOperation)
    {
      final CompareOperation op = (CompareOperation) operation;
      return new CompareRequestProtocolOp(op.getRawEntryDN(), op
          .getRawAttributeType(), op.getAssertionValue());
    }
    else if (operation instanceof DeleteOperation)
    {
      final DeleteOperation op = (DeleteOperation) operation;
      return new DeleteRequestProtocolOp(op.getRawEntryDN());
    }
    else if (operation instanceof ExtendedOperation)
    {
      final ExtendedOperation op = (ExtendedOperation) operation;
      return new ExtendedRequestProtocolOp(op.getRequestOID(), op
          .getRequestValue());
    }
    else if (operation instanceof ModifyDNOperation)
    {
      final ModifyDNOperation op = (ModifyDNOperation) operation;
      return new ModifyDNRequestProtocolOp(op.getRawEntryDN(), op
          .getRawNewRDN(), op.deleteOldRDN(), op.getRawNewSuperior());
    }
    else if (operation instanceof ModifyOperation)
    {
      final ModifyOperation op = (ModifyOperation) operation;
      return new ModifyRequestProtocolOp(op.getRawEntryDN(), op
          .getRawModifications());
    }
    else if (operation instanceof SearchOperation)
    {
      final SearchOperation op = (SearchOperation) operation;
      return new SearchRequestProtocolOp(op.getRawBaseDN(), op.getScope(), op
          .getDerefPolicy(), op.getSizeLimit(), op.getTimeLimit(), op
          .getTypesOnly(), op.getRawFilter(), op.getAttributes());
    }
    else if (operation instanceof UnbindOperation)
    {
      return new UnbindRequestProtocolOp();
    }
    throw new RuntimeException("Not implemented for operation " + operation);
  }

  @Override
  public LdapPromise<Void> abandonAsync(AbandonRequest request)
  {
    final int messageID = nextMessageID.getAndIncrement();
    return enqueueOperation(new AbandonOperationBasis(clientConnection, messageID, messageID,
        to(request.getControls()), request.getRequestID()));
  }

  @Override
  public LdapPromise<Result> addAsync(AddRequest request, IntermediateResponseHandler intermediateResponseHandler)
  {
    final int messageID = nextMessageID.getAndIncrement();
    return enqueueOperation(new AddOperationBasis(clientConnection, messageID, messageID, to(request.getControls()),
        valueOfObject(request.getName()), to(request.getAllAttributes())));
  }

  @Override
  public void addConnectionEventListener(ConnectionEventListener listener)
  {
    // not useful so far
  }

  @Override
  public LdapPromise<BindResult> bindAsync(BindRequest request,
      IntermediateResponseHandler intermediateResponseHandler)
  {
    final int messageID = nextMessageID.getAndIncrement();
    String userName = request.getName();
    byte[] password = ((SimpleBindRequest) request).getPassword();
    return enqueueOperation(new BindOperationBasis(clientConnection, messageID, messageID, to(request.getControls()),
        "3", ByteString.valueOfUtf8(userName), ByteString.wrap(password)));
  }

  @Override
  public void close(UnbindRequest request, String reason)
  {
    AuthenticationInfo authInfo = this.clientConnection.getAuthenticationInfo();
    if (authInfo != null && authInfo.isAuthenticated())
    {
      final int messageID = nextMessageID.getAndIncrement();
      final UnbindOperationBasis operation = new UnbindOperationBasis(
          clientConnection, messageID, messageID, to(request.getControls()));
      operation.setInnerOperation(this.clientConnection.isInnerConnection());

      // run synchronous
      operation.run();
    }
    else
    {
      this.clientConnection.disconnect(DisconnectReason.UNBIND, false, null);
    }

    // At this point, we try to log the request with OK status code.
    // If it was already logged, it will be a no op.
    this.clientConnection.log(HttpServletResponse.SC_OK);

    isClosed = true;
  }

  @Override
  public LdapPromise<CompareResult> compareAsync(CompareRequest request,
      IntermediateResponseHandler intermediateResponseHandler)
  {
    final int messageID = nextMessageID.getAndIncrement();
    return enqueueOperation(new CompareOperationBasis(clientConnection, messageID, messageID,
        to(request.getControls()), valueOfObject(request.getName()),
        request.getAttributeDescription().getAttributeType().getOID(),
        request.getAssertionValue()));
  }

  @Override
  public LdapPromise<Result> deleteAsync(DeleteRequest request,
      IntermediateResponseHandler intermediateResponseHandler)
  {
    final int messageID = nextMessageID.getAndIncrement();
    return enqueueOperation(new DeleteOperationBasis(clientConnection, messageID, messageID,
        to(request.getControls()), valueOfObject(request.getName())));
  }

  @Override
  public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(ExtendedRequest<R> request,
      IntermediateResponseHandler intermediateResponseHandler)
  {
    final int messageID = nextMessageID.getAndIncrement();
    ExtendedOperation op = new ExtendedOperationBasis(
        clientConnection, messageID, messageID, to(request.getControls()), request.getOID(), request.getValue());
    op.setAuthorizationEntry(clientConnection.getAuthenticationInfo().getAuthorizationEntry());
    return enqueueOperation(op);
  }

  /**
   * Return the queueing strategy used by this connection.
   *
   * @return The queueing strategy used by this connection
   */
  public QueueingStrategy getQueueingStrategy()
  {
    return queueingStrategy;
  }

  @Override
  public boolean isClosed()
  {
    return isClosed;
  }

  @Override
  public boolean isValid()
  {
    return this.clientConnection.isConnectionValid();
  }

  @Override
  public LdapPromise<Result> modifyAsync(ModifyRequest request,
      IntermediateResponseHandler intermediateResponseHandler)
  {
    final int messageID = nextMessageID.getAndIncrement();
    return enqueueOperation(new ModifyOperationBasis(clientConnection, messageID, messageID,
        to(request.getControls()), to(request.getName()),
        toModifications(request.getModifications())));
  }

  @Override
  public LdapPromise<Result> modifyDNAsync(ModifyDNRequest request,
      IntermediateResponseHandler intermediateResponseHandler)
  {
    final int messageID = nextMessageID.getAndIncrement();
    return enqueueOperation(new ModifyDNOperationBasis(clientConnection, messageID, messageID,
        to(request.getControls()), to(request.getName()), to(request
            .getNewRDN()), request.isDeleteOldRDN(), to(request
            .getNewSuperior())));
  }

  @Override
  public void removeConnectionEventListener(ConnectionEventListener listener)
  {
    // not useful so far
  }

  @Override
  public LdapPromise<Result> searchAsync(final SearchRequest request,
      final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler)
  {
    final int messageID = nextMessageID.getAndIncrement();
    return enqueueOperation(new SearchOperationBasis(clientConnection, messageID, messageID,
        to(request.getControls()), to(request.getName()),
        request.getScope(), request.getDereferenceAliasesPolicy(),
        request.getSizeLimit(), request.getTimeLimit(),
        request.isTypesOnly(), toSearchFilter(request.getFilter()),
        new LinkedHashSet<String>(request.getAttributes())), entryHandler);
  }

  @Override
  public String toString()
  {
    return this.clientConnection.toString();
  }
}
