/*
 * 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
 *
 *
 *      Copyright 2013-2015 ForgeRock AS.
 */
package org.forgerock.opendj.adapter.server3x;

import java.net.InetAddress;
import java.net.UnknownHostException;

import org.forgerock.opendj.ldap.AbstractSynchronousConnection;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionEventListener;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.controls.Control;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.BindClient;
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.GenericBindRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.SASLBindRequest;
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.GenericExtendedResult;
import org.forgerock.opendj.ldap.responses.Responses;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.util.promise.Promise;
import org.opends.server.core.AddOperation;
import org.opends.server.core.BindOperation;
import org.opends.server.core.CompareOperation;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.ExtendedOperation;
import org.opends.server.core.ModifyDNOperation;
import org.opends.server.core.ModifyOperation;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchListener;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.Requests;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;

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.util.promise.Promises.*;

/**
 * This class provides a connection factory and an adapter for the OpenDJ 2.x
 * server.
 */
public final class Adapters {

    /**
     * Constructor.
     */
    private Adapters() {
        // No implementation required.
    }

    /**
     * Returns a new root connection factory.
     *
     * @return A new root connection factory.
     */
    public static ConnectionFactory newRootConnectionFactory() {
        InternalClientConnection icc = InternalClientConnection.getRootConnection();
        return newConnectionFactory(icc);
    }

    /**
     * Returns a new anonymous connection factory.
     *
     * @return A new anonymous connection factory.
     */
    public static ConnectionFactory newAnonymousConnectionFactory() {
        InternalClientConnection icc = new InternalClientConnection(new AuthenticationInfo());
        return newConnectionFactory(icc);
    }

    /**
     * Returns a new connection factory for a specified user.
     *
     * @param userDN
     *            The specified user's DN.
     * @return a new connection factory.
     */
    public static ConnectionFactory newConnectionFactoryForUser(final DN userDN) {
        InternalClientConnection icc = null;
        try {
            icc = new InternalClientConnection(to(userDN));
        } catch (DirectoryException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        return newConnectionFactory(icc);
    }

    /**
     * Returns a new connection factory.
     *
     * @param icc
     *            The internal client connection from server side.
     * @return A new SDK connection factory.
     */
    public static ConnectionFactory newConnectionFactory(final InternalClientConnection icc) {
        return new ConnectionFactory() {

            @Override
            public void close() {
                // Nothing to do.
            }

            @Override
            public Promise<Connection, LdapException> getConnectionAsync() {
                // TODO change the path...
                return newResultPromise(newConnection(icc));
            }

            @Override
            public Connection getConnection() throws LdapException {
                return newConnection(icc);
            }
        };
    }

    /**
     * Returns a new root connection.
     *
     * @return A new root connection.
     */
    public static Connection newRootConnection() {
        return newConnection(InternalClientConnection.getRootConnection());
    }

    /**
     * Returns a new connection for an anonymous user.
     *
     * @return A new connection.
     */
    public static Connection newAnonymousConnection() {
        return newConnection(new InternalClientConnection(new AuthenticationInfo()));
    }

    /**
     * Returns a new connection for a specified user.
     *
     * @param dn
     *            The DN of the user.
     * @return A new connection for a specified user.
     * @throws LdapException
     *             If no such object.
     */
    public static Connection newConnectionForUser(final DN dn) throws LdapException {
        try {
            return newConnection(new InternalClientConnection(to(dn)));
        } catch (DirectoryException e) {
            throw newLdapException(Responses.newResult(ResultCode.NO_SUCH_OBJECT));
        }
    }

    private static Connection newConnection(final InternalClientConnection icc) {
        return new AbstractSynchronousConnection() {

            @Override
            public Result search(final SearchRequest request, final SearchResultHandler handler)
                    throws LdapException {
                InternalSearchListener internalSearchListener = new InternalSearchListener() {

                    @Override
                    public void handleInternalSearchReference(
                            InternalSearchOperation searchOperation,
                            SearchResultReference searchReference) throws DirectoryException {
                        handler.handleReference(from(searchReference));
                    }

                    @Override
                    public void handleInternalSearchEntry(InternalSearchOperation searchOperation,
                            SearchResultEntry searchEntry) throws DirectoryException {
                        handler.handleEntry(from(searchEntry));
                    }
                };

                final SearchFilter filter = toSearchFilter(request.getFilter());
                final org.opends.server.protocols.internal.SearchRequest sr =
                    Requests.newSearchRequest(to(request.getName()), request.getScope(), filter)
                        .setDereferenceAliasesPolicy(request.getDereferenceAliasesPolicy())
                        .setSizeLimit(request.getSizeLimit())
                        .setTimeLimit(request.getTimeLimit())
                        .setTypesOnly(request.isTypesOnly())
                        .addAttribute(request.getAttributes())
                        .addControl(to(request.getControls()));
                return getResponseResult(icc.processSearch(sr, internalSearchListener));
            }

            @Override
            public void removeConnectionEventListener(ConnectionEventListener listener) {
                // Internal client connection don't have any connection events.
            }

            @Override
            public Result modifyDN(final ModifyDNRequest request) throws LdapException {
                final ModifyDNOperation modifyDNOperation =
                        icc.processModifyDN(valueOfObject(request.getName()),
                                valueOfObject(request.getNewRDN()),
                                request.isDeleteOldRDN(),
                                request.getNewSuperior() != null ? valueOfObject(request.getNewSuperior()) : null,
                                to(request.getControls()));
                return getResponseResult(modifyDNOperation);
            }

            @Override
            public Result modify(final ModifyRequest request) throws LdapException {
                final ModifyOperation modifyOperation =
                        icc.processModify(valueOfObject(request.getName()), toRawModifications(request
                                .getModifications()), to(request.getControls()));
                return getResponseResult(modifyOperation);
            }

            @Override
            public boolean isValid() {
                // Always true.
                return true;
            }

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

            @Override
            public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request,
                    final IntermediateResponseHandler handler) throws LdapException {

                final ExtendedOperation extendedOperation =
                        icc.processExtendedOperation(request.getOID(), request.getValue(),
                                to(request.getControls()));

                final Result result = getResponseResult(extendedOperation);
                final GenericExtendedResult genericExtendedResult =
                        Responses.newGenericExtendedResult(result.getResultCode())
                                .setDiagnosticMessage(result.getDiagnosticMessage()).setMatchedDN(
                                        result.getMatchedDN()).setValue(
                                        extendedOperation.getResponseValue().toByteString());
                try {
                    R extendedResult =
                            request.getResultDecoder().decodeExtendedResult(genericExtendedResult,
                                    new DecodeOptions());
                    for (final Control control : result.getControls()) {
                        extendedResult.addControl(control);
                    }
                    return extendedResult;

                } catch (DecodeException e) {
                    org.opends.server.types.DN matchedDN = extendedOperation.getMatchedDN();
                    return request.getResultDecoder().newExtendedErrorResult(
                            extendedOperation.getResultCode(),
                            matchedDN != null ? matchedDN.toString() : null,
                            extendedOperation.getErrorMessage().toString());
                }
            }

            @Override
            public Result delete(final DeleteRequest request) throws LdapException {
                final DeleteOperation deleteOperation =
                        icc.processDelete(valueOfObject(request.getName()), to(request.getControls()));
                return getResponseResult(deleteOperation);
            }

            @Override
            public CompareResult compare(final CompareRequest request) throws LdapException {
                final CompareOperation compareOperation =
                        icc.processCompare(valueOfObject(request.getName()),
                                request.getAttributeDescription().toString(),
                                request.getAssertionValue(), to(request.getControls()));

                CompareResult result = Responses.newCompareResult(compareOperation.getResultCode());
                return getResponseResult(compareOperation, result);
            }

            @Override
            public void close(final UnbindRequest request, final String reason) {
                // no implementation in open-ds.
            }

            @Override
            public BindResult bind(final BindRequest request) throws LdapException {
                BindOperation bindOperation = null;
                if (request instanceof SimpleBindRequest) {
                    bindOperation =
                            icc.processSimpleBind(valueOfUtf8(request.getName()),
                                    ByteString.wrap(((SimpleBindRequest) request).getPassword()),
                                    to(request.getControls()));
                } else if (request instanceof SASLBindRequest) {
                    String serverName = null;
                    try {
                        serverName = InetAddress.getByName(null).getCanonicalHostName();
                    } catch (UnknownHostException e) {
                        // nothing to do.
                    }
                    BindClient bindClient = request.createBindClient(serverName);
                    do {
                        final GenericBindRequest genericBindRequest = bindClient.nextBindRequest();
                        bindOperation =
                                icc.processSASLBind(
                                        valueOfUtf8(request.getName()),
                                        ((SASLBindRequest) request).getSASLMechanism(),
                                        getCredentials(genericBindRequest.getAuthenticationValue()),
                                        to(request.getControls()));
                    } while (bindOperation.getResultCode() == ResultCode.SASL_BIND_IN_PROGRESS);

                    bindClient.dispose();

                } else { // not supported
                    throw newLdapException(Responses.newResult(ResultCode.AUTH_METHOD_NOT_SUPPORTED));
                }
                BindResult result = Responses.newBindResult(bindOperation.getResultCode());
                result.setServerSASLCredentials(bindOperation.getSASLCredentials());

                if (result.isSuccess()) {
                    return result;
                } else {
                    throw newLdapException(result);
                }
            }

            @Override
            public void addConnectionEventListener(ConnectionEventListener listener) {
                // Internal client connection don't have any connection events.
            }

            @Override
            public Result add(final AddRequest request) throws LdapException {
                final AddOperation addOperation =
                        icc.processAdd(valueOfObject(request.getName()), to(request
                                .getAllAttributes()), to(request.getControls()));
                return getResponseResult(addOperation);
            }

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