package net.lightbody.bmp.util;

import net.lightbody.bmp.exception.UnsupportedCharsetException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;

import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMessage;

/**
 * Utility class to assist with manipulation of {@link io.netty.handler.codec.http.HttpObject} instances, including
 * {@link HttpMessage} and {@link HttpContent}.
 */
public class HttpObjectUtil {
    private static final Logger log = LoggerFactory.getLogger(HttpObjectUtil.class);

    /**
     * Replaces the entity body of the message with the specified contents. Encodes the message contents according to charset in the message's
     * Content-Type header, or uses {@link BrowserMobHttpUtil#DEFAULT_HTTP_CHARSET} if none is specified.
     * <b>Note:</b> If the charset of the message is not supported on this platform, this will throw an {@link java.nio.charset.UnsupportedCharsetException}.
     *
     * TODO: Currently this method only works for FullHttpMessages, since it must modify the Content-Length header; determine if this may be applied to chunked messages as well
     *
     * @param message the HTTP message to manipulate
     * @param newContents the new entity body contents
     * @throws java.nio.charset.UnsupportedCharsetException if the charset in the message is not supported on this platform
     */
    public static void replaceTextHttpEntityBody(FullHttpMessage message, String newContents) {
        // get the content type for this message so we can encode the newContents into a byte stream appropriately
        String contentTypeHeader = message.headers().get(HttpHeaders.Names.CONTENT_TYPE);

        Charset messageCharset;
        try {
            messageCharset = BrowserMobHttpUtil.readCharsetInContentTypeHeader(contentTypeHeader);
        } catch (UnsupportedCharsetException e) {
            java.nio.charset.UnsupportedCharsetException cause = e.getUnsupportedCharsetExceptionCause() ;
            log.error("Found unsupported character set in Content-Type header '{}' while attempting to replace contents of HTTP message.", contentTypeHeader, cause);

            throw cause;
        }

        if (messageCharset == null) {
            messageCharset = BrowserMobHttpUtil.DEFAULT_HTTP_CHARSET;
            log.warn("No character set declared in HTTP message. Replacing text using default charset {}.", messageCharset);
        }

        byte[] contentBytes = newContents.getBytes(messageCharset);

        replaceBinaryHttpEntityBody(message, contentBytes);
    }

    /**
     * Replaces an HTTP entity body with the specified binary contents.
     * TODO: Currently this method only works for FullHttpMessages, since it must modify the Content-Length header; determine if this may be applied to chunked messages as well
     *
     * @param message the HTTP message to manipulate
     * @param newBinaryContents the new entity body contents
     */
    public static void replaceBinaryHttpEntityBody(FullHttpMessage message, byte[] newBinaryContents) {
        message.content().resetWriterIndex();
        // resize the buffer if needed, since the new message may be longer than the old one
        message.content().ensureWritable(newBinaryContents.length, true);
        message.content().writeBytes(newBinaryContents);

        // update the Content-Length header, since the size may have changed
        message.headers().set(HttpHeaders.Names.CONTENT_LENGTH, newBinaryContents.length);
    }

    /**
     * Extracts the entity body from an HTTP content object, according to the specified character set. The character set cannot be null. If
     * the character set is not specified or is unknown, you still must specify a suitable default charset (see {@link BrowserMobHttpUtil#DEFAULT_HTTP_CHARSET}).
     *
     * @param httpContent HTTP content object to extract the entity body from
     * @param charset character set of the entity body
     * @return String representation of the entity body
     * @throws IllegalArgumentException if the charset is null
     */
    public static String extractHttpEntityBody(HttpContent httpContent, Charset charset) {
        if (charset == null) {
            throw new IllegalArgumentException("No charset specified when extracting the contents of an HTTP message");
        }

        byte[] contentBytes = BrowserMobHttpUtil.extractReadableBytes(httpContent.content());

        return new String(contentBytes, charset);
    }

    /**
     * Extracts the entity body from a FullHttpMessage, according to the character set in the message's Content-Type header. If the Content-Type
     * header is not present or does not specify a charset, assumes the ISO-8859-1 character set (see {@link BrowserMobHttpUtil#DEFAULT_HTTP_CHARSET}).
     *
     * @param httpMessage HTTP message to extract entity body from
     * @return String representation of the entity body
     * @throws java.nio.charset.UnsupportedCharsetException if there is a charset specified in the content-type header, but it is not supported
     */
    public static String extractHttpEntityBody(FullHttpMessage httpMessage) {
        Charset charset;
        try {
            charset = getCharsetFromMessage(httpMessage);
        } catch (UnsupportedCharsetException e) {
            // the declared character set is not supported, so it is impossible to decode the contents of the message. log an error and throw an exception
            // to alert the client code.
            java.nio.charset.UnsupportedCharsetException cause = e.getUnsupportedCharsetExceptionCause();

            String contentTypeHeader = HttpHeaders.getHeader(httpMessage, HttpHeaders.Names.CONTENT_TYPE);
            log.error("Cannot retrieve text contents of message because HTTP message declares a character set that is not supported on this platform. Content type header: {}.", contentTypeHeader, cause);

            throw cause;
        }

        return extractHttpEntityBody(httpMessage, charset);
    }

    /**
     * Derives the charset from the Content-Type header in the HttpMessage. If the Content-Type header is not present or does not contain
     * a character set, this method returns the ISO-8859-1 character set. See {@link BrowserMobHttpUtil#readCharsetInContentTypeHeader(String)}
     * for more details.
     *
     * @param httpMessage HTTP message to extract charset from
     * @return the charset associated with the HTTP message, or the default charset if none is present
     * @throws UnsupportedCharsetException if there is a charset specified in the content-type header, but it is not supported
     */
    public static Charset getCharsetFromMessage(HttpMessage httpMessage) throws UnsupportedCharsetException {
        String contentTypeHeader = HttpHeaders.getHeader(httpMessage, HttpHeaders.Names.CONTENT_TYPE);

        Charset charset = BrowserMobHttpUtil.readCharsetInContentTypeHeader(contentTypeHeader);
        if (charset == null) {
            return BrowserMobHttpUtil.DEFAULT_HTTP_CHARSET;
        }

        return charset;
    }

    /**
     * Extracts the binary contents from an HTTP message.
     *
     * @param httpContent HTTP content object to extract the entity body from
     * @return binary contents of the HTTP message
     */
    public static byte[] extractBinaryHttpEntityBody(HttpContent httpContent) {
        return BrowserMobHttpUtil.extractReadableBytes(httpContent.content());
    }
}
