/**
 * Copyright 2013 Netflix, 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 rx.apache.http;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.nio.client.HttpAsyncClient;
import org.apache.http.nio.client.methods.HttpAsyncMethods;
import org.apache.http.nio.protocol.HttpAsyncRequestProducer;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.BasicHttpContext;

import rx.Observable;
import rx.Observable.OnSubscribeFunc;
import rx.Observer;
import rx.Subscription;
import rx.apache.http.consumers.ResponseConsumerDelegate;
import rx.subscriptions.CompositeSubscription;
import rx.subscriptions.Subscriptions;

/**
 * An {@link Observable} interface to Apache {@link HttpAsyncClient}.
 * <p>
 * The initial {@link HttpResponse} is returned via {@link Observer#onNext} wrapped in a {@link ObservableHttpResponse}.
 * <p>
 * The content stream is retrieved from {@link ObservableHttpResponse#getContent()}.
 * <p>
 * It is aware of Content-Type <i>text/event-stream</i> and will stream each event via {@link Observer#onNext}.
 * <p>
 * Other Content-Types will be returned as a single call to {@link Observer#onNext}.
 * <p>
 * Examples:
 * <p>
 * <pre> {@code
 * ObservableHttp.createGet("http://www.wikipedia.com", httpClient).toObservable();
 * } </pre>
 * <p>
 * <pre> {@code
 * ObservableHttp.createRequest(HttpAsyncMethods.createGet("http://www.wikipedia.com"), httpClient).toObservable();
 * } </pre>
 * 
 * An {@link HttpClient} can be created like this:
 * 
 * <pre> {@code
 * CloseableHttpAsyncClient httpClient = HttpAsyncClients.createDefault();
 * httpClient.start(); // start it
 * httpClient.stop(); // stop it
 * } </pre>
 * <p>
 * A client with custom configurations can be created like this:
 * </p>
 * <pre> {@code
 * final RequestConfig requestConfig = RequestConfig.custom()
 *     .setSocketTimeout(1000)
 *     .setConnectTimeout(200).build();
 * final CloseableHttpAsyncClient httpClient = HttpAsyncClients.custom()
 *     .setDefaultRequestConfig(requestConfig)
 *     .setMaxConnPerRoute(20)
 *     .setMaxConnTotal(50)
 *     .build();
 * httpClient.start();
 * }</pre>
 * <p>
 * 
 * @param <T>
 */
public class ObservableHttp<T> {

    private final OnSubscribeFunc<T> onSubscribe;

    private ObservableHttp(OnSubscribeFunc<T> onSubscribe) {
        this.onSubscribe = onSubscribe;
    }

    private static <T> ObservableHttp<T> create(OnSubscribeFunc<T> onSubscribe) {
        return new ObservableHttp<T>(onSubscribe);
    }

    public Observable<T> toObservable() {
        return Observable.create(new OnSubscribeFunc<T>() {

            @Override
            public Subscription onSubscribe(Observer<? super T> observer) {
                return onSubscribe.onSubscribe(observer);
            }
        });
    }

    public static ObservableHttp<ObservableHttpResponse> createGet(String uri, final HttpAsyncClient client) {
        return createRequest(HttpAsyncMethods.createGet(uri), client);
    }

    /**
     * Execute request using {@link HttpAsyncRequestProducer} to define HTTP Method, URI and payload (if applicable).
     * <p>
     * If the response is chunked (or flushed progressively such as with <i>text/event-stream</i> <a href="http://www.w3.org/TR/2009/WD-eventsource-20091029/">Server-Sent Events</a>) this will call
     * {@link Observer#onNext} multiple times.
     * <p>
     * Use {@code HttpAsyncMethods.create* } factory methods to create {@link HttpAsyncRequestProducer} instances.
     * <p>
     * A client can be retrieved like this:
     * <p>
     * <pre> {@code      CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault(); } </pre> 
     * <p>
     * A client with custom configurations can be created like this:
     * </p>
     * <pre> {@code
     * final RequestConfig requestConfig = RequestConfig.custom()
     *     .setSocketTimeout(3000)
     *     .setConnectTimeout(3000).build();
     * final CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom()
     *     .setDefaultRequestConfig(requestConfig)
     *     .setMaxConnPerRoute(20)
     *     .setMaxConnTotal(50)
     *     .build();
     * httpclient.start();
     * }</pre>
     * 
     * 
     * @param requestProducer
     * @param client
     * @return
     */
    public static ObservableHttp<ObservableHttpResponse> createRequest(final HttpAsyncRequestProducer requestProducer, final HttpAsyncClient client) {
        return createRequest(requestProducer, client, new BasicHttpContext());
    }

    /**
     * Execute request using {@link HttpAsyncRequestProducer} to define HTTP Method, URI and payload (if applicable).
     * <p>
     * If the response is chunked (or flushed progressively such as with <i>text/event-stream</i> <a href="http://www.w3.org/TR/2009/WD-eventsource-20091029/">Server-Sent Events</a>) this will call
     * {@link Observer#onNext} multiple times.
     * <p>
     * Use {@code HttpAsyncMethods.create* } factory methods to create {@link HttpAsyncRequestProducer} instances.
     * <p>
     * A client can be retrieved like this:
     * <p>
     * <pre> {@code      CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault(); } </pre>
     * <p>
     * A client with custom configurations can be created like this:
     * </p>
     * <pre> {@code
     * final RequestConfig requestConfig = RequestConfig.custom()
     *     .setSocketTimeout(3000)
     *     .setConnectTimeout(3000).build();
     * final CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom()
     *     .setDefaultRequestConfig(requestConfig)
     *     .setMaxConnPerRoute(20)
     *     .setMaxConnTotal(50)
     *     .build();
     * httpclient.start();
     * }</pre>
     *
     *
     * @param requestProducer
     * @param client
     * @param context The HttpContext
     * @return
     */
    public static ObservableHttp<ObservableHttpResponse> createRequest(final HttpAsyncRequestProducer requestProducer, final HttpAsyncClient client, final HttpContext context) {

        return ObservableHttp.create(new OnSubscribeFunc<ObservableHttpResponse>() {

            @Override
            public Subscription onSubscribe(final Observer<? super ObservableHttpResponse> observer) {

                final CompositeSubscription parentSubscription = new CompositeSubscription();

                // return a Subscription that wraps the Future so it can be cancelled
                parentSubscription.add(Subscriptions.from(client.execute(requestProducer, new ResponseConsumerDelegate(observer, parentSubscription),
                        context, new FutureCallback<HttpResponse>() {

                            @Override
                            public void completed(HttpResponse result) {
                                observer.onCompleted();
                            }

                            @Override
                            public void failed(Exception ex) {
                                observer.onError(ex);
                            }

                            @Override
                            public void cancelled() {
                                observer.onCompleted();
                            }

                        })));

                return parentSubscription;
            }
        });
    }

}
