package com.jscape.inet.http.protocol;

import com.jscape.inet.http.protocol.marshaling.ChunkDecodingInputStream;
import com.jscape.inet.http.protocol.marshaling.ChunkEncodingInputStream;
import com.jscape.inet.http.protocol.marshaling.ClientCodec;
import com.jscape.inet.http.protocol.marshaling.ContentHeader;
import com.jscape.inet.http.protocol.messages.*;
import com.jscape.util.IOTools;
import com.jscape.util.io.DataCodec;
import com.jscape.util.io.LimitedInputStream;
import com.jscape.util.io.NullInputStream;
import com.jscape.util.net.TransportAddress;
import com.jscape.util.net.connection.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.jscape.inet.http.protocol.HttpClient.OperationException.wrap;
import static com.jscape.util.Parameters.assertNotNull;

/**
 * @author Alex Usun
 */
public class HttpClient
        implements AutoCloseable {
    
    protected static final int BUFFER_SIZE = 64 * 1024;
    protected static final int CHUNK_SIZE = 4096;
    
    protected final Connector<SyncRawConnection> connector;
    protected final ConnectionProtector connectionProtector;
    protected final DataCodec<Message> messageCodec;
    protected final Logger logger;
    protected SyncMessageConnectionSyncRawBase<Message> connection;
    protected InputStream connectionIn;
    protected OutputStream connectionOut;
    
    public static HttpClient defaultClientFor(Connector<SyncRawConnection> connector, ConnectionProtector connectionProtector, Logger logger) {
        return new HttpClient(connector, connectionProtector, ClientCodec.defaultCodec(), logger);
    }

    public HttpClient(Connector<SyncRawConnection> connector,
                      ConnectionProtector connectionProtector,
                      DataCodec<Message> messageCodec,
                      Logger logger) {
        assertNotNull(connector);
        this.connector = connector;

        assertNotNull(connectionProtector);
        this.connectionProtector = connectionProtector;

        assertNotNull(messageCodec);
        this.messageCodec = messageCodec;
        
        assertNotNull(logger);
        this.logger = logger;
    }

    public TransportAddress localAddress() {
        return this.connection != null ? this.connection.localAddress() : null;
    }

    public TransportAddress remoteAddress() {
        return this.connection != null ? this.connection.remoteAddress() : null;
    }

    public HttpClient open()
            throws OperationException {
        SyncRawConnection baseConnection = null;

        try {
            baseConnection = this.connector.connect();
            this.connectionProtector.protect(baseConnection);

            this.connection = new SyncMessageConnectionSyncRawBase<>(baseConnection, this.messageCodec);
            this.connectionIn = new SyncRawConnection.ConnectionInputStream(baseConnection);
            this.connectionOut = new SyncRawConnection.ConnectionOutputStream(baseConnection);
            
            logConnectionCreated();

            return this;
        } catch (Exception e) {
            ConnectionTools.close(baseConnection);
            throw wrap(e);
        }
    }

    public void close() {
        if (this.connection != null && !this.connection.closed()) {
            this.connection.close();
            logConnectionClosed();
        }
    }
    
    public Response connect(Request request, IntermediateResponseListener listener)
            throws IOException {
        try {
            InputStream requestMessageBody = messageBodyFor(request);
            ConnectRequest requestMessage = new ConnectRequest(request.uri, request.protocolVersion, request.headers.toArray(), requestMessageBody);
            
            return exchange(requestMessage, listener);
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    public Response head(Request request, IntermediateResponseListener listener)
            throws IOException {
        try {
            InputStream requestMessageBody = messageBodyFor(request);
            HeadRequest requestMessage = new HeadRequest(request.uri, request.protocolVersion, request.headers.toArray(), requestMessageBody);

            Response response = exchange(requestMessage, listener);

            return new Response(response.protocolVersion, response.statusCode, response.reasonPhrase, response.headers, new NullInputStream());
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    public Response options(Request request, IntermediateResponseListener listener)
            throws IOException {
        try {
            InputStream requestMessageBody = messageBodyFor(request);
            OptionsRequest requestMessage = new OptionsRequest(request.uri, request.protocolVersion, request.headers.toArray(), requestMessageBody);

            return exchange(requestMessage, listener);
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    public Response trace(Request request, IntermediateResponseListener listener)
            throws IOException {
        try {
            InputStream requestMessageBody = messageBodyFor(request);
            TraceRequest requestMessage = new TraceRequest(request.uri, request.protocolVersion, request.headers.toArray(), requestMessageBody);

            return exchange(requestMessage, listener);
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    public Response get(Request request, IntermediateResponseListener listener)
            throws IOException {
        try {
            InputStream requestMessageBody = messageBodyFor(request);
            GetRequest requestMessage = new GetRequest(request.uri, request.protocolVersion, request.headers.toArray(), requestMessageBody);

            return exchange(requestMessage, listener);
        } catch (Exception e) {
            throw wrap(e);
        }
    }
    
    public Response patch(Request request, IntermediateResponseListener listener)
            throws IOException {
        try {
            InputStream requestMessageBody = messageBodyFor(request);
            PatchRequest requestMessage = new PatchRequest(request.uri, request.protocolVersion, request.headers.toArray(), requestMessageBody);

            return exchange(requestMessage, listener);
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    public Response put(Request request, IntermediateResponseListener listener)
            throws IOException {
        try {
            InputStream requestMessageBody = messageBodyFor(request);
            PutRequest requestMessage = new PutRequest(request.uri, request.protocolVersion, request.headers.toArray(), requestMessageBody);

            return exchange(requestMessage, listener);
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    public Response post(Request request, IntermediateResponseListener listener)
            throws IOException {
        try {
            InputStream requestMessageBody = messageBodyFor(request);
            PostRequest requestMessage = new PostRequest(request.uri, request.protocolVersion, request.headers.toArray(), requestMessageBody);

            return exchange(requestMessage, listener);
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    public Response delete(Request request, IntermediateResponseListener listener)
            throws IOException {
        try {
            InputStream requestMessageBody = messageBodyFor(request);
            DeleteRequest requestMessage = new DeleteRequest(request.uri, request.protocolVersion, request.headers.toArray(), requestMessageBody);

            return exchange(requestMessage, listener);
        } catch (Exception e) {
            throw wrap(e);
        }
    }
    
    public InputStream inputStream() {
        return this.connectionIn;
    }
    
    public OutputStream outputStream() {
        return this.connectionOut;
    }

    protected Response exchange(RequestMessage request, IntermediateResponseListener listener)
            throws Exception {
        send(request);
        Response response = receive();

        if (response.intermediate()) {
            listener.onResponse(this, response);
            IOTools.copy(request.body, this.connectionOut, BUFFER_SIZE);
            this.connectionOut.flush();

            response = receive();
        }

        return response;
    }
    
    protected void send(RequestMessage message)
            throws Exception {
        logOutgoingMessage(message);
        this.connection.write(message);
    }

    protected Response receive()
            throws Exception {
        ResponseMessage message = (ResponseMessage) this.connection.read();
        logIncomingMessage(message);

        Response response = responseFor(message);
        assertNoErrors(response);

        return response;
    }

    protected Response responseFor(ResponseMessage message)
            throws Exception {
        Headers headers = new Headers(message.headers);
        Headers trailerHeaders = new Headers();
        InputStream body = message.body;
    
        Long contentLength = ContentHeader.CONTENT_LENGTH.of(headers);
    
        if (StatusCode.contentNotExpected(message.statusCode)) {
            body = new NullInputStream();
        } else if (contentLength != null) {
            body = new LimitedInputStream(body, contentLength, false);
        } else if (ContentHeader.TRANSFER_ENCODING_CHUNKED.of(headers)) {
            body = new ChunkDecodingInputStream(body, trailerHeaders);
        }

        return new Response(message.protocolVersion, message.statusCode, message.reasonPhrase, headers, body, trailerHeaders);
    }
    
    protected InputStream messageBodyFor(Request request) {
        Long contentLength = ContentHeader.CONTENT_LENGTH.of(request.headers);
    
        if (contentLength != null) {
            return new LimitedInputStream(request.body, contentLength, false);
        } else if (ContentHeader.TRANSFER_ENCODING_CHUNKED.of(request.headers)) {
            return new ChunkEncodingInputStream(request.body, CHUNK_SIZE, request.trailerHeaders);
        }
        
        return request.body;
    }

    protected void assertNoErrors(Response response)
            throws ServerException {
        if (response.error()) {
            throw new ServerException(response);
        }
    }

    protected void logConnectionCreated() {
        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.log(Level.FINE, String.format("HTTP connection created: [%s <-> %s].",
                                                      this.connection.localAddress(),
                                                      this.connection.remoteAddress()));
        }
    }

    protected void logConnectionClosed() {
        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.log(Level.FINE, String.format("HTTP connection closed: [%s <-> %s].",
                                                      this.connection.localAddress(),
                                                      this.connection.remoteAddress()));
        }
    }

    protected void logIncomingMessage(Message message) {
        if (this.logger.isLoggable(Level.FINER)) {
            this.logger.log(Level.FINER, String.format("Incoming HTTP message [%s <-> %s]: %s.",
                                                       this.connection.localAddress(),
                                                       this.connection.remoteAddress(),
                                                       message));
        }
    }

    protected void logOutgoingMessage(Message message) {
        if (this.logger.isLoggable(Level.FINER)) {
            this.logger.log(Level.FINER, String.format("Outgoing HTTP message [%s <-> %s]: %s.",
                                                       this.connection.localAddress(),
                                                       this.connection.remoteAddress(),
                                                       message));
        }
    }

    public interface IntermediateResponseListener {

        IntermediateResponseListener NULL = new IntermediateResponseListener() {
            @Override
            public void onResponse(HttpClient client, Response response) {
                // empty
            }
        };

        void onResponse(HttpClient client, Response response);

    }

    public static class OperationException
            extends IOException {

        public static OperationException wrap(Throwable e) {
            return e instanceof OperationException ? (OperationException) e : new OperationException(e);
        }

        public OperationException() {
        }

        public OperationException(String message) {
            super(message);
        }

        public OperationException(String message, Throwable cause) {
            super(message, cause);
        }

        public OperationException(Throwable cause) {
            super(cause);
        }
    }

    public static class ServerException
            extends OperationException {

        public final Response response;

        public ServerException(Response response) {
            super(String.format("%s (%s)", response.reasonPhrase, response.statusCode));
            
            assertNotNull(response);
            this.response = response;
        }
    }
}
