/*
 * Copyright 2016 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 io.reactivex.netty.protocol.http.client.internal;

import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.reactivex.netty.channel.Connection;
import io.reactivex.netty.channel.FlushSelectorOperator;
import rx.Observable;
import rx.functions.Func1;

import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;

public final class RawRequest<I, O> {

    private final Redirector<I, O> redirector;
    private final HttpRequest headers;
    @SuppressWarnings("rawtypes")
    private final Observable content;
    private final Func1<?, Boolean> flushSelector;
    private final boolean hasTrailers;

    @SuppressWarnings("rawtypes")
    private RawRequest(HttpRequest headers, Observable content, Func1<?, Boolean> flushSelector, boolean hasTrailers,
                       Redirector<I, O> redirector) {
        this.headers = headers;
        this.content = content;
        this.flushSelector = flushSelector;
        this.hasTrailers = hasTrailers;
        this.redirector = redirector;
    }

    public RawRequest<I, O> addHeader(CharSequence name, Object value) {
        HttpRequest headersCopy = _copyHeaders();
        headersCopy.headers().add(name, value);
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> addHeaders(Map<? extends CharSequence, ? extends Iterable<Object>> headers) {
        HttpRequest headersCopy = _copyHeaders();
        for (Entry<? extends CharSequence, ? extends Iterable<Object>> header : headers.entrySet()) {
            headersCopy.headers().add(header.getKey(), header.getValue());
        }
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> addHeaderValues(CharSequence name, Iterable<Object> values) {
        HttpRequest headersCopy = _copyHeaders();
        headersCopy.headers().add(name, values);
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> addCookie(Cookie cookie) {
        String cookieHeader = ClientCookieEncoder.STRICT.encode(cookie);
        return addHeader(HttpHeaderNames.COOKIE, cookieHeader);
    }

    public RawRequest<I, O> addDateHeader(CharSequence name, Date value) {
        HttpRequest headersCopy = _copyHeaders();
        headersCopy.headers().add(name, value);
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> addDateHeader(CharSequence name, Iterable<Date> values) {
        HttpRequest headersCopy = _copyHeaders();
        for (Date value : values) {
            headersCopy.headers().add(name, value);
        }
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> setDateHeader(CharSequence name, Date value) {
        HttpRequest headersCopy = _copyHeaders();
        headersCopy.headers().set(name, value);
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> setHeader(CharSequence name, Object value) {
        HttpRequest headersCopy = _copyHeaders();
        headersCopy.headers().set(name, value);
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> setHeaders(Map<? extends CharSequence, ? extends Iterable<Object>> headers) {
        HttpRequest headersCopy = _copyHeaders();
        for (Entry<? extends CharSequence, ? extends Iterable<Object>> header : headers.entrySet()) {
            headersCopy.headers().set(header.getKey(), header.getValue());
        }
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> setHeaderValues(CharSequence name, Iterable<Object> values) {
        HttpRequest headersCopy = _copyHeaders();
        headersCopy.headers().set(name, values);
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> setDateHeader(CharSequence name, Iterable<Date> values) {
        HttpRequest headersCopy = _copyHeaders();
        boolean addNow = false;
        for (Date value : values) {
            if (addNow) {
                headersCopy.headers().add(name, value);
            } else {
                headersCopy.headers().set(name, value);
                addNow = true;
            }
        }
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> setKeepAlive(boolean keepAlive) {
        HttpRequest headersCopy = _copyHeaders();
        HttpUtil.setKeepAlive(headersCopy, keepAlive);
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> setTransferEncodingChunked() {
        HttpRequest headersCopy = _copyHeaders();
        HttpUtil.setTransferEncodingChunked(headersCopy, true);
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> removeHeader(CharSequence name) {
        HttpRequest headersCopy = _copyHeaders();
        headersCopy.headers().remove(name);
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> setMethod(HttpMethod method) {
        HttpRequest headersCopy = _copyHeaders(headers.uri(), method);
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> setUri(String uri) {
        HttpRequest headersCopy = _copyHeaders(uri, headers.method());
        return new RawRequest<>(headersCopy, content, flushSelector, hasTrailers, redirector);
    }

    public RawRequest<I, O> followRedirect(Redirector<I, O> redirectHandler) {
        return new RawRequest<>(headers, content, flushSelector, hasTrailers, redirectHandler);
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    public Observable asObservable(Connection<?, ?> connection) {
        HttpRequest headers = this.headers;
        if (null == content) {
            headers = _copyHeaders();
            headers.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
        }

        Observable toReturn = Observable.just(headers);

        if (null != content) {
            if (null == flushSelector) {
                toReturn = toReturn.concatWith(content);
            } else {
                toReturn = toReturn.concatWith(content.lift(new FlushSelectorOperator(flushSelector, connection)));
            }
        }

        if (!hasTrailers) {
            toReturn = toReturn.concatWith(Observable.just(LastHttpContent.EMPTY_LAST_CONTENT));
        }

        return toReturn;
    }

    private HttpRequest _copyHeaders() {
        return _copyHeaders(headers.uri(), headers.method());
    }

    private HttpRequest _copyHeaders(String uri, HttpMethod method) {
        final HttpRequest newHeaders = new DefaultHttpRequest(headers.protocolVersion(), method, uri);
        // TODO: May be we can optimize this by not copying
        for (Entry<String, String> header : headers.headers()) {
            newHeaders.headers().set(header.getKey(), header.getValue());
        }
        return newHeaders;
    }

    public static <I, O> RawRequest<I, O> create(HttpVersion version, HttpMethod httpMethod, String uri,
                                                 Redirector<I, O> redirectHandler) {
        final HttpRequest headers = new DefaultHttpRequest(version, httpMethod, uri);
        return create(headers, null, null, false, redirectHandler);
    }

    @SuppressWarnings("rawtypes")
    public static <I, O> RawRequest<I, O> create(HttpRequest headers, Observable content, boolean hasTrailers,
                                                 Redirector<I, O> redirectHandler) {
        return create(headers, content, null, hasTrailers, redirectHandler);
    }

    @SuppressWarnings("rawtypes")
    public static <I, O> RawRequest<I, O>  create(HttpRequest headers, Observable content,
                                                  Func1<?, Boolean> flushSelector, boolean hasTrailers,
                                                  Redirector<I, O> redirectHandler) {
        return new RawRequest<>(headers, content, flushSelector, hasTrailers, redirectHandler);
    }

    public HttpRequest getHeaders() {
        return headers;
    }

    @SuppressWarnings("rawtypes")
    public Observable getContent() {
        return content;
    }

    public Func1<?, Boolean> getFlushSelector() {
        return flushSelector;
    }

    public boolean hasTrailers() {
        return hasTrailers;
    }

    public Redirector<I, O> getRedirector() {
        return redirector;
    }
}
