package com.atlassian.bitbucket.io;

import org.apache.commons.lang3.StringUtils;
import org.mozilla.universalchardet.UniversalDetector;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.URLConnection;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

public class ContentDetectionUtils {

    /**
     * Tries to detect the encoding of the data, first using byte order marks if present and then by applying
     * heuristics to the first {@code bufferSize} bytes.
     *
     * @param stream     the stream containing the data
     * @param bufferSize the number of bytes to read from the stream when applying heuristic type detection
     * @return the detected character set, which may be {@code null} if
     * @throws IOException if bytes cannot be read from the provided {@code stream}
     * @throws NullPointerException if the provided {@code stream} is {@code null}
     */
    @Nullable
    public static String detectEncoding(@Nonnull BufferedInputStream stream, int bufferSize) throws IOException {
        checkNotNull(stream, "stream");
        checkArgument(bufferSize > 0, "");

        //The first check, we only read in the first 4 bytes to check for the presence of byte order marks.
        byte[] buf = new byte[4];
        stream.mark(5);
        int read = stream.read(buf);
        stream.reset();

        UniversalDetector detector = new UniversalDetector(null);
        detector.handleData(buf, 0, read);
        if (!detector.isDone()) {
            //If we make it here, that means the file doesn't begin with byte order marks specifying a UTF-8, 16 or
            //32. That means we need to detect a bit harder.
            detector.reset(); //Reset the detector; it's going to get the same 4 first bytes over

            buf = new byte[bufferSize];
            stream.mark(buf.length); //Mark the stream so we can reset for reading content

            //Spool over the file until we've read out the maximum number of bytes we're prepared to, or until the
            //detection logic decides it's figured out the character set
            int remaining = buf.length;
            while (remaining > 0 && !detector.isDone() && (read = stream.read(buf, 0, remaining)) > 0) {
                detector.handleData(buf, 0, read);
                remaining -= read;
            }
            stream.reset(); //Reset the stream so the caller can read content

            detector.dataEnd(); //We're not going to provide any more bytes, so go with the best guess
        }

        //Return whatever we decided (which may be null)
        return detector.getDetectedCharset();
    }

    /**
     * Tries to detect whether the data is binary or not. When there is no clear indication of whether the content is
     * binary or text, {@code false} is returned.
     *
     * @param stream     the stream containing the data
     * @param bufferSize the maximum number of bytes to read from the stream for detection (must be greater than 0)
     * @return {@code true} if binary is detected, {@code false} otherwise
     * @throws IOException if bytes cannot be read from the provided {@code stream}
     */
    public static boolean isBinary(@Nonnull BufferedInputStream stream, int bufferSize) throws IOException {
        checkNotNull(stream, "stream");
        checkArgument(bufferSize > 0, "");

        //check for byte Order Marker on UTF-8, 16, 32 files
        byte[] buf = new byte[4];
        stream.mark(5);
        int len = stream.read(buf);
        stream.reset();

        UniversalDetector detector = new UniversalDetector(null);
        detector.handleData(buf, 0, len);
        if (detector.isDone()) {
            return false;
        }

        //Not UTF check ASCII text
        stream.mark(bufferSize);
        len = 0;
        int b;
        while ((b = stream.read()) != -1 && len < (bufferSize - 192)) {
            len++;
            if (b == 0) {
                stream.reset();
                return true;
            }
        }
        stream.reset();
        return false;
    }


    /**
     * Tries detect the content type of the data, first by looking at the first few bytes in the input stream and, if
     * not successful, by looking at the path (if one was provided).
     *
     * @param stream the stream containing the data
     * @param path   the path to use to infer the content type by file extension
     * @return the content type for the data, or {@code null} if the type cannot be detected
     * @throws IOException if bytes cannot be read from the provided {@code stream}
     * @throws NullPointerException if the provided {@code stream} is {@code null}
     */
    @Nullable
    public static String detectContentType(@Nonnull BufferedInputStream stream, @Nullable String path)
            throws IOException {
        // First try inspecting the stream. The method below properly handles the necessary mark()/reset()
        String type = URLConnection.guessContentTypeFromStream(checkNotNull(stream, "stream"));
        if (StringUtils.isNotBlank(path)) {
            if (type == null) {
                // No luck with the stream; try guessing the content type from the path
                type = URLConnection.guessContentTypeFromName(path);
            } else if ("application/xml".equals(type)) {
                // If the stream content type is application/xml, then we check the filename as well, as the true
                // content type may be more specific. For example, "image/svg+xml".
                String pathType = URLConnection.guessContentTypeFromName(path);
                if (StringUtils.isNotBlank(pathType) && pathType.endsWith("+xml")) {
                    type = pathType;
                }
            }
        }

        return type;
    }
}
