/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2012-2015 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package org.glassfish.jersey.test.inmemory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.security.Principal;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;

import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.client.ClientResponse;
import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
import org.glassfish.jersey.client.spi.Connector;
import org.glassfish.jersey.client.spi.ConnectorProvider;
import org.glassfish.jersey.internal.MapPropertiesDelegate;
import org.glassfish.jersey.internal.PropertiesDelegate;
import org.glassfish.jersey.message.internal.OutboundMessageContext;
import org.glassfish.jersey.server.ApplicationHandler;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ContainerResponse;
import org.glassfish.jersey.server.spi.ContainerResponseWriter;

import jersey.repackaged.com.google.common.collect.Lists;
import jersey.repackaged.com.google.common.util.concurrent.MoreExecutors;

/**
 * In-memory client connector.
 *
 * @author Pavel Bucek (pavel.bucek at oracle.com)
 * @author Marek Potociar (marek.potociar at oracle.com)
 */
class InMemoryConnector implements Connector {

    private static final Logger LOGGER = Logger.getLogger(InMemoryConnector.class.getName());

    private final URI baseUri;
    private final ApplicationHandler appHandler;

    /**
     * In-memory client connector provider.
     */
    static class Provider implements ConnectorProvider {

        private final URI baseUri;
        private final ApplicationHandler appHandler;

        /**
         * Create new in-memory connector provider.
         *
         * @param baseUri    application base URI.
         * @param appHandler RequestInvoker instance which represents application.
         */
        Provider(URI baseUri, ApplicationHandler appHandler) {
            this.baseUri = baseUri;
            this.appHandler = appHandler;
        }

        @Override
        public Connector getConnector(Client client, Configuration config) {
            return new InMemoryConnector(baseUri, appHandler);
        }
    }

    /**
     * Create new in-memory connector.
     *
     * @param baseUri    application base URI.
     * @param appHandler RequestInvoker instance which represents application.
     */
    private InMemoryConnector(final URI baseUri, final ApplicationHandler appHandler) {
        this.baseUri = baseUri;
        this.appHandler = appHandler;
    }

    /**
     * In memory container response writer.
     */
    public static class InMemoryResponseWriter implements ContainerResponseWriter {

        private MultivaluedMap<String, String> headers;
        private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        private boolean committed;
        private Response.StatusType statusInfo;

        @Override
        public OutputStream writeResponseStatusAndHeaders(long contentLength, ContainerResponse responseContext) {
            List<Object> length = Lists.newArrayList();
            length.add(String.valueOf(contentLength));

            responseContext.getHeaders().put(HttpHeaders.CONTENT_LENGTH, length);
            headers = responseContext.getStringHeaders();
            statusInfo = responseContext.getStatusInfo();
            return baos;
        }

        @Override
        public boolean suspend(long timeOut, TimeUnit timeUnit, TimeoutHandler timeoutHandler) {
            LOGGER.warning("Asynchronous server side invocations are not supported by InMemoryContainer.");
            return false;
        }

        @Override
        public void setSuspendTimeout(long timeOut, TimeUnit timeUnit) {
            throw new UnsupportedOperationException("Async server side invocations are not supported by InMemoryContainer.");
        }

        @Override
        public void commit() {
            committed = true;
        }

        @Override
        public void failure(Throwable error) {
            throw new ProcessingException("Server-side request processing failed with an error.", error);
        }

        @Override
        public boolean enableResponseBuffering() {
            return true;
        }

        /**
         * Get the written entity.
         *
         * @return Byte array which contains the entity written by the server.
         */
        public byte[] getEntity() {
            if (!committed) {
                throw new IllegalStateException("Response is not committed yet.");
            }
            return baos.toByteArray();
        }

        /**
         * Return response headers.
         *
         * @return headers.
         */
        public MultivaluedMap<String, String> getHeaders() {
            return headers;
        }

        /**
         * Returns response status info.
         *
         * @return status info.
         */
        public Response.StatusType getStatusInfo() {
            return statusInfo;
        }
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Transforms client-side request to server-side and invokes it on provided application ({@link ApplicationHandler}
     * instance).
     *
     * @param clientRequest client side request to be invoked.
     */
    @Override
    public ClientResponse apply(final ClientRequest clientRequest) {
        PropertiesDelegate propertiesDelegate = new MapPropertiesDelegate();

        final ContainerRequest containerRequest = new ContainerRequest(baseUri,
                clientRequest.getUri(), clientRequest.getMethod(),
                null, propertiesDelegate);

        containerRequest.getHeaders().putAll(clientRequest.getStringHeaders());

        final ByteArrayOutputStream clientOutput = new ByteArrayOutputStream();
        if (clientRequest.getEntity() != null) {
            clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
                @Override
                public OutputStream getOutputStream(int contentLength) throws IOException {
                    final MultivaluedMap<String, Object> clientHeaders = clientRequest.getHeaders();
                    if (contentLength != -1 && !clientHeaders.containsKey(HttpHeaders.CONTENT_LENGTH)) {
                        containerRequest.getHeaders().putSingle(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
                    }
                    return clientOutput;
                }
            });
            clientRequest.enableBuffering();

            try {
                clientRequest.writeEntity();
            } catch (IOException e) {
                final String msg = "Error while writing entity to the output stream.";
                LOGGER.log(Level.SEVERE, msg, e);
                throw new ProcessingException(msg, e);
            }
        }

        containerRequest.setEntityStream(new ByteArrayInputStream(clientOutput.toByteArray()));

        boolean followRedirects = ClientProperties.getValue(clientRequest.getConfiguration().getProperties(),
                ClientProperties.FOLLOW_REDIRECTS, true);

        final InMemoryResponseWriter inMemoryResponseWriter = new InMemoryResponseWriter();
        containerRequest.setWriter(inMemoryResponseWriter);
        containerRequest.setSecurityContext(new SecurityContext() {
            @Override
            public Principal getUserPrincipal() {
                return null;
            }

            @Override
            public boolean isUserInRole(String role) {
                return false;
            }

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

            @Override
            public String getAuthenticationScheme() {
                return null;
            }
        });
        appHandler.handle(containerRequest);

        return tryFollowRedirects(followRedirects,
                createClientResponse(
                        clientRequest,
                        inMemoryResponseWriter),
                new ClientRequest(clientRequest));

    }

    @Override
    public Future<?> apply(final ClientRequest request, final AsyncConnectorCallback callback) {
        return MoreExecutors.sameThreadExecutor().submit(new Runnable() {
            @Override
            public void run() {
                try {
                    callback.response(apply(request));
                } catch (ProcessingException ex) {
                    throw ex;
                } catch (Throwable t) {
                    callback.failure(t);
                }
            }
        });
    }

    @Override
    public void close() {
        // do nothing
    }

    private ClientResponse createClientResponse(final ClientRequest clientRequest,
                                                final InMemoryResponseWriter responseWriter) {
        final ClientResponse clientResponse = new ClientResponse(responseWriter.getStatusInfo(), clientRequest);
        clientResponse.getHeaders().putAll(responseWriter.getHeaders());
        clientResponse.setEntityStream(new ByteArrayInputStream(responseWriter.getEntity()));
        return clientResponse;
    }

    @SuppressWarnings("MagicNumber")
    private ClientResponse tryFollowRedirects(boolean followRedirects, ClientResponse response, ClientRequest request) {
        if (!followRedirects) {
            return response;
        }

        while (true) {
            switch (response.getStatus()) {
                case 303:
                case 302:
                case 307:
                    request = new ClientRequest(request);
                    request.setUri(response.getLocation());
                    if (response.getStatus() == 303) {
                        request.setMethod("GET");
                    }
                    response = apply(request);
                    break;
                default:
                    return response;
            }
        }
    }

    @Override
    public String getName() {
        return "Jersey InMemory Connector";
    }
}
