/*
 * Copyright (c) 2015 Twitter, Inc. All rights reserved.
 * Licensed under the Apache License v2.0
 * http://www.apache.org/licenses/LICENSE-2.0
 */

package com.twitter.whiskey.demo;

import com.twitter.whiskey.net.Header;
import com.twitter.whiskey.net.Headers;
import com.twitter.whiskey.net.Request;
import com.twitter.whiskey.net.ResponseFuture;
import com.twitter.whiskey.net.WhiskeyClient;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.SocketPermission;
import java.net.URL;
import java.nio.ByteBuffer;
import java.security.Permission;
import java.security.Principal;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;

/**
 * Implementation of Http(s)UrlConnection using Whiskey.
 *
 * Please note that using WhiskeyClient directly to issue requests affords
 * the user a significantly more flexible API than this legacy interface
 * supports. This implementation is provided as a demonstration of the
 * libary's ability to support a wide variety of interfaces.
 *
 * @author Michael Schore
 */
public class HttpUrlConnectionImpl extends HttpURLConnection {

    private final AtomicBoolean submitted = new AtomicBoolean(false);
    private final WhiskeyClient client;
    private final Request.Builder requestBuilder;
    private Request request;
    private ResponseFuture responseFuture;

    /**
     * Constructor for the HttpURLConnection.
     *
     * @param url the URL                                                                             c
     */
    public HttpUrlConnectionImpl(URL url, WhiskeyClient client) {
        super(url);
        requestBuilder = new Request.Builder(url);
        if (!getFollowRedirects()) requestBuilder.maxRedirects(0);
        this.client = client;
    }

    @Override
    public void disconnect() {
    }

    @Override
    public void setConnectTimeout(int timeout) {
    }

    @Override
    public int getConnectTimeout() {
        return -1;
    }

    @Override
    public void setReadTimeout(int timeout) {
    }

    @Override
    public int getReadTimeout() {
        return -1;
    }

    @Override
    public void setFixedLengthStreamingMode (int contentLength) {
        requestBuilder.replaceHeader("content-length", String.valueOf(contentLength));
    }

    public void setFixedLengthStreamingMode(long contentLength) {
        requestBuilder.replaceHeader("content-length", String.valueOf(contentLength));
    }

    @Override
    public void setChunkedStreamingMode(int chunklen) {
        requestBuilder.removeHeader("content-length");
    }

    @Override
    public void setInstanceFollowRedirects(boolean followRedirects) {

        super.setInstanceFollowRedirects(followRedirects);
        requestBuilder.maxRedirects(followRedirects ? 20 : 0);
    }

    @Override
    public void setRequestMethod(String method) throws ProtocolException {

        super.setRequestMethod(method);
        try {
            requestBuilder.method(Request.Method.fromString(method));
        } catch (IllegalArgumentException e) {
            throw new ProtocolException("Invalid HTTP method: " + method);
        }
    }

    @Override
    public void setRequestProperty(String key, String value) {

        if (value == null) {
            requestBuilder.removeHeader(key);
            return;
        }
        requestBuilder.replaceHeader(key, value);
    }

    @Override
    public void addRequestProperty(String key, String value) {
        requestBuilder.addHeader(key, value);
    }

    @Override
    public void setIfModifiedSince(long ifmodifiedsince) {

        requestBuilder.removeHeader("if-modified-since");
        requestBuilder.addHeader(new Header("if-modified-since", new Date(ifmodifiedsince)));
    }

    @Override
    public OutputStream getOutputStream() throws IOException {

        PipedOutputStream out = new PipedOutputStream();
        PipedInputStream in = new PipedInputStream();
        in.connect(out);
        requestBuilder.body(in);
        return out;
    }

    @Override
    public Map<String, List<String>> getRequestProperties() {
        return request.getHeaders().map();
    }

    @Override
    public String getRequestProperty(String key) {
        return request.getHeaders().getFirst(key);
    }

    @Override
    public Permission getPermission() throws IOException {
        return new SocketPermission(url.getHost() + ":" + url.getPort(), "connect, resolve");
    }

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

    @Override
    public void connect() throws IOException {
        // No op
    }

    public ResponseFuture getResponseFuture() {
        if (submitted.compareAndSet(false, true)) {
            request = requestBuilder.create();
            responseFuture = client.submit(request);
            connected = true;
        }
        return responseFuture;
    }

    @Override
    public InputStream getErrorStream() {
        return null;
    }

    @Override
    public int getResponseCode() throws IOException {
        try {
            responseCode = getResponseFuture().get().getStatusCode();
            return responseCode;
        } catch (InterruptedException | ExecutionException e) {
            throw new IOException(e);
        }
    }

    @Override
    public String getContentEncoding() {
        try {
            return getResponseFuture().getHeadersFuture().get()
                .getFirst(Headers.CONTENT_ENCODING);
        } catch (InterruptedException | ExecutionException | NumberFormatException e) {
            return null;
        }
    }

    @Override
    public int getContentLength() {
        try {
            return Integer.parseInt(getResponseFuture().getHeadersFuture().get()
                .getFirst(Headers.CONTENT_LENGTH));
        } catch (InterruptedException | ExecutionException | NumberFormatException e) {
            return -1;
        }
    }

    @Override
    public String getContentType() {
        try {
            return getResponseFuture().getHeadersFuture().get()
                .getFirst(Headers.CONTENT_TYPE);
        } catch (InterruptedException | ExecutionException | NumberFormatException e) {
            return null;
        }
    }

    public Header getHeader(int n) {

        try {
            int i = 0;
            for (Header header : getResponseFuture().getHeadersFuture().get().entries()) {
                if (i++ == n) return header;
            }
        } catch (InterruptedException | ExecutionException ignored) {
        }

        return null;
    }

    @Override
    public String getHeaderField(String name) {

        try {
            return getResponseFuture().getHeadersFuture().get().getFirst(name);
        } catch (InterruptedException | ExecutionException e) {
            return null;
        }
    }

    @Override
    public String getHeaderFieldKey(int n) {

        Header header = getHeader(n);
        return header == null ? null : header.getKey();
    }

    @Override
    public String getHeaderField(int n) {

        Header header = getHeader(n);
        return header == null ? null : header.getValue();
    }

    @Override
    public Map<String, List<String>> getHeaderFields() {

        final Headers headers = getHeaders();
        return headers == null ? Collections.<String, List<String>>emptyMap() : headers.map();
    }

    public Headers getHeaders() {

        try {
            return getResponseFuture().getHeadersFuture().get();
        } catch (InterruptedException | ExecutionException e) {
            return null;
        }
    }

    @Override
    public InputStream getInputStream() throws IOException {

        if (!doInput) throw new UnsupportedOperationException();

        return new InputStream() {
            Iterator<ByteBuffer> iterator = getResponseFuture().getBodyFuture().iterator();
            private ByteBuffer currentBuffer;
            @Override
            public int read() throws IOException {
                try {
                    if (currentBuffer != null && currentBuffer.remaining() > 0) {
                        return currentBuffer.get() & 0xff;
                    }

                    while (iterator.hasNext()) {
                        currentBuffer = iterator.next();
                        if (currentBuffer != null && currentBuffer.remaining() > 0) {
                            return currentBuffer.get() & 0xff;
                        }
                    }

                    return -1;
                } catch (RuntimeException e) {
                    throw new IOException(e);
                }
            }
        };
    }

    public ByteBuffer getBody() {

        try {
            return getResponseFuture().getBodyFuture().get();
        } catch (InterruptedException | ExecutionException e) {
            return null;
        }
    }


//    @Override
//    public String getCipherSuite() {
//        return null;
//    }
//
//    @Override
//    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
//        return null;
//    }
//
//    @Override
//    public Principal getLocalPrincipal() {
//        return null;
//    }
//
//    @Override
//    public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
//    }
//
//    @Override
//    public HostnameVerifier getHostnameVerifier() {
//        return null;
//    }
//
//    @Override
//    public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
//        throw new UnsupportedOperationException("use SSLContext via WhiskeyClient configuration to set TLS parameters");
//    }
//
//    @Override
//    public SSLSocketFactory getSSLSocketFactory() {
//        return null;
//    }
//
//    @Override
//    public Certificate[] getLocalCertificates() {
//        return new Certificate[0];
//    }
//
//    @Override
//    public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
//        return new Certificate[0];
//    }
}