/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.component.netty4.http;

import java.net.URI;
import java.util.List;
import java.util.Map;

import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.util.ReferenceCountUtil;
import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
import org.apache.camel.component.netty4.NettyConfiguration;
import org.apache.camel.component.netty4.NettyConstants;
import org.apache.camel.component.netty4.NettyProducer;
import org.apache.camel.http.common.cookie.CookieHandler;
import org.apache.camel.support.SynchronizationAdapter;


/**
 * HTTP based {@link NettyProducer}.
 */
public class NettyHttpProducer extends NettyProducer {

    public NettyHttpProducer(NettyHttpEndpoint nettyEndpoint, NettyConfiguration configuration) {
        super(nettyEndpoint, configuration);
    }

    @Override
    public NettyHttpEndpoint getEndpoint() {
        return (NettyHttpEndpoint) super.getEndpoint();
    }

    @Override
    public NettyHttpConfiguration getConfiguration() {
        return (NettyHttpConfiguration) super.getConfiguration();
    }

    @Override
    public boolean process(Exchange exchange, AsyncCallback callback) {
        return super.process(exchange, new NettyHttpProducerCallback(exchange, callback, getConfiguration()));
    }

    @Override
    protected Object getRequestBody(Exchange exchange) throws Exception {
        // creating the url to use takes 2-steps
        final NettyHttpEndpoint endpoint = getEndpoint();
        final String uri = NettyHttpHelper.createURL(exchange, endpoint);
        final URI u = NettyHttpHelper.createURI(exchange, uri, endpoint);

        final NettyHttpBinding nettyHttpBinding = endpoint.getNettyHttpBinding();
        final HttpRequest request = nettyHttpBinding.toNettyRequest(exchange.getIn(), u.toString(), getConfiguration());
        exchange.getIn().setHeader(Exchange.HTTP_URL, uri);
        // Need to check if we need to close the connection or not
        if (!HttpUtil.isKeepAlive(request)) {
            // just want to make sure we close the channel if the keepAlive is not true
            exchange.setProperty(NettyConstants.NETTY_CLOSE_CHANNEL_WHEN_COMPLETE, true);
        }
        if (getConfiguration().isBridgeEndpoint()) {
            // Need to remove the Host key as it should be not used when bridging/proxying
            exchange.getIn().removeHeader("host");
        }

        final CookieHandler cookieHandler = endpoint.getCookieHandler();
        if (cookieHandler != null) {
            Map<String, List<String>> cookieHeaders = cookieHandler.loadCookies(exchange, u);
            for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
                String key = entry.getKey();
                if (entry.getValue().size() > 0) {
                    request.headers().add(key, entry.getValue());
                }
            }
        }

        return request;
    }

    /**
     * Callback that ensures the channel is returned to the pool when we are done.
     */
    private final class NettyHttpProducerCallback implements AsyncCallback {

        private final Exchange exchange;
        private final AsyncCallback callback;
        private final NettyHttpConfiguration configuration;

        private NettyHttpProducerCallback(Exchange exchange, AsyncCallback callback, NettyHttpConfiguration configuration) {
            this.exchange = exchange;
            this.callback = callback;
            this.configuration = configuration;
        }

        @Override
        public void done(boolean doneSync) {
            try {
                // only handle when we are done asynchronous as then the netty producer is done sending, and we have a response
                if (!doneSync) {
                    NettyHttpMessage nettyMessage = exchange.hasOut() ? exchange.getOut(NettyHttpMessage.class) : exchange.getIn(NettyHttpMessage.class);
                    if (nettyMessage != null) {
                        final FullHttpResponse response = nettyMessage.getHttpResponse();
                        // Need to retain the ByteBuffer for producer to consumer
                        if (response != null) {
                            response.content().retain();

                            // need to release the response when we are done
                            exchange.addOnCompletion(new SynchronizationAdapter() {
                                @Override
                                public void onDone(Exchange exchange) {
                                    if (response.refCnt() > 0) {
                                        log.debug("Releasing Netty HttpResonse ByteBuf");
                                        ReferenceCountUtil.release(response);
                                    }
                                }
                            });

                            // the actual url is stored on the IN message in the getRequestBody method as its accessed on-demand
                            String actualUrl = exchange.getIn().getHeader(Exchange.HTTP_URL, String.class);
                            int code = response.status() != null ? response.status().code() : -1;
                            log.debug("Http responseCode: {}", code);

                            // if there was a http error code then check if we should throw an exception
                            boolean ok = NettyHttpHelper.isStatusCodeOk(code, configuration.getOkStatusCodeRange());
                            if (!ok && getConfiguration().isThrowExceptionOnFailure()) {
                                // operation failed so populate exception to throw
                                Exception cause = NettyHttpHelper.populateNettyHttpOperationFailedException(exchange, actualUrl, response, code, getConfiguration().isTransferException());
                                exchange.setException(cause);
                            }
                        }
                    }
                }
            } finally {
                // ensure we call the delegated callback
                callback.done(doneSync);
            }
        }
    }
}
