package com.atlassian.bitbucket.mesh.git.transcode;

import com.atlassian.bitbucket.mesh.git.GitTimers;
import com.atlassian.bitbucket.mesh.io.EncodingUtils;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.layout.StorageLayout;
import com.atlassian.util.profiling.Ticker;
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import org.apache.commons.io.input.CloseShieldInputStream;
import org.apache.commons.io.output.CloseShieldOutputStream;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

public class DefaultTranscoder implements Transcoder {

    static final String TRANSCODED_BINARY = "Dropping BINARY output for: ";

    private static final int BUFFER_SIZE_BINARY = 8192;
    private static final int BUFFER_SIZE_ENCODING = 32768;

    private final StorageLayout storageLayout;

    public DefaultTranscoder(StorageLayout storageLayout) {
        this.storageLayout = storageLayout;
    }

    @Override
    public void transcode(@Nonnull String pathString, @Nonnull OutputStream outputStream) throws IOException {
        checkArgument(StringUtils.isNotBlank(pathString), "No path was provided for transcoding.");
        requireNonNull(outputStream, "outputStream");

        Path path = Paths.get(pathString);
        MoreFiles.requireWithin(path, storageLayout.getTempDir());

        try (Ticker ignored = GitTimers.TRANSCODE.start()) {
            transcodePath(path, outputStream);
        }
    }

    private static String detectEncoding(InputStream inputStream) throws IOException {
        try (Ticker ignored = GitTimers.TRANSCODE.start("detect encoding")) {
            return EncodingUtils.detectEncoding(inputStream, BUFFER_SIZE_ENCODING);
        }
    }

    private static boolean isBinary(InputStream inputStream) throws IOException {
        try (Ticker ignored = GitTimers.TRANSCODE.start("detect binary")) {
            return EncodingUtils.isBinary(inputStream, BUFFER_SIZE_BINARY);
        }
    }

    private static void transcodePath(Path path, OutputStream outputStream) throws IOException {
        try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(path), BUFFER_SIZE_ENCODING)) {
            if (isBinary(inputStream)) {
                String reason = TRANSCODED_BINARY + path;
                // This does not close the outputStream because we don't own it.
                outputStream.write(reason.getBytes(StandardCharsets.UTF_8));
            } else {
                transcodeStreams(inputStream, outputStream);
            }
        }
    }

    private static void transcodeStreams(InputStream inputStream, OutputStream outputStream) throws IOException {
        String encoding = detectEncoding(inputStream);
        if (encoding == null || "UTF-8".equals(encoding)) {
            //If we cannot detect an encoding, or the file is already UTF-8 encoded, just stream the file directly
            try (Ticker ignored = GitTimers.TRANSCODE.start("stream")) {
                // Don't close input or output streams, we don't own them
                ByteStreams.copy(inputStream, outputStream);
            }
        } else {
            //Otherwise, transcode the file to UTF-8 by reading it using the detected encoding and writing it using
            //UTF-8 encoding.
            try (Ticker ignored = GitTimers.TRANSCODE.start("apply")) {
                new ByteSource() {

                    @Nonnull
                    @Override
                    public InputStream openStream() {
                        return CloseShieldInputStream.wrap(inputStream);
                    }
                }.asCharSource(Charset.forName(encoding)).copyTo(
                        new ByteSink() {

                            @Nonnull
                            @Override
                            public OutputStream openStream() {
                                return CloseShieldOutputStream.wrap(outputStream);
                            }
                        }.asCharSink(StandardCharsets.UTF_8));
            }
        }
    }
}
