package com.atlassian.stash.internal.scm.git.transcode;

import com.atlassian.bitbucket.Product;
import com.atlassian.bitbucket.io.ContentDetectionUtils;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.util.Timer;
import com.atlassian.bitbucket.util.TimerUtils;
import com.atlassian.stash.internal.scm.git.command.diff.DiffOutputHandler;
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 com.google.common.base.Preconditions.checkNotNull;

/**
 * @since 3.1
 */
@SuppressWarnings("unused") //Profiling timers
public class DefaultTranscoder implements Transcoder {

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

    private final ApplicationPropertiesService propertiesService;

    public DefaultTranscoder(ApplicationPropertiesService propertiesService) {
        this.propertiesService = propertiesService;
    }

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

        Path path = Paths.get(pathString);
        if (path.startsWith(propertiesService.getTempDir().getAbsolutePath())) {
            try (Timer timer = TimerUtils.start("git: transcode")) {
                transcodePath(path, outputStream);
            }
        } else {
            throw new IllegalArgumentException("The specified path, " + path + ", does not belong to " +
                    Product.NAME + " and cannot be transcoded.");
        }
    }

    private static String detectEncoding(BufferedInputStream inputStream) throws IOException {
        try (Timer timer = TimerUtils.start("git: transcode: detect encoding")) {
            return ContentDetectionUtils.detectEncoding(inputStream, BUFFER_SIZE_ENCODING);
        }
    }

    private static boolean isBinary(BufferedInputStream inputStream) throws IOException {
        try (Timer timer = TimerUtils.start("git: transcode: detect binary")) {
            return ContentDetectionUtils.isBinary(inputStream, BUFFER_SIZE_BINARY);
        }
    }

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

    private void transcodeStreams(BufferedInputStream 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 (Timer timer = TimerUtils.start("git: transcode: 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 (Timer timer = TimerUtils.start("git: transcode: apply")) {
                new ByteSource() {

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

                            @Override
                            public OutputStream openStream() throws IOException {
                                return new CloseShieldOutputStream(outputStream);
                            }
                        }.asCharSink(StandardCharsets.UTF_8));
            }
        }
    }
}
