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

import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilder;
import com.atlassian.bitbucket.mesh.process.handler.AbstractProcessStdioHandler;
import com.atlassian.bitbucket.mesh.util.Person;

import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.stripEnd;

public class DefaultGitMailmapResolver implements AutoCloseable, MailmapResolver {

    private final GitProcessBuilder processBuilder;

    private volatile CheckMailmapStdioHandler handler;

    public DefaultGitMailmapResolver(GitProcessBuilder processBuilder) {
        this.processBuilder = processBuilder;
    }

    @Override
    public void close() {
        CheckMailmapStdioHandler mapper = this.handler;
        if (mapper != null) {
            mapper.close();
            this.handler = null;
        }
    }

    @Nonnull
    @Override
    public Person resolve(@Nonnull Person person) {
        requireNonNull(person, "person");
        ensureStarted();

        try {
            return handler.resolve(person);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return person;
        }
    }

    private void ensureStarted() {
        if (handler == null) {
            synchronized (this) {
                if (handler == null) {
                    handler = new CheckMailmapStdioHandler();
                    processBuilder.command("check-mailmap")
                            .argument("--stdin")
                            .build(handler)
                            .start();
                }
            }
        }
    }

    /**
     * A handler that manages both {@code stdin} and {@code stdout} to provide contact details to map and then
     * parses the mapped results. A {@link BlockingQueue} is used to transfer results from the pump thread to
     * the request thread.
     */
    private static class CheckMailmapStdioHandler extends AbstractProcessStdioHandler<Void> {

        private static final Person UNRESOLVED = new Person("unresolved", "marker");

        private final BlockingQueue<Person> out;

        private volatile boolean running;

        CheckMailmapStdioHandler() {
            out = new ArrayBlockingQueue<>(1);
            running = true;
        }

        public void close() {
            if (running) {
                closeStdin();
                running = false;
            }
        }

        @Override
        public Void getResult() {
            return null;
        }

        @Override
        public void onExit(int exitCode) {
            //If the process terminates (for example, if check-mailmap can't parse the contact),
            //ensure the running flag is set to false
            running = false;
        }

        @Override
        public void onStdout(@Nonnull ByteBuffer buffer, boolean closed) {
            Person resolved;
            if (buffer.hasRemaining()) {
                CharBuffer decoded = UTF_8.decode(buffer);

                //Drop the trailing newline, if there is one
                int lastChar = decoded.limit() - 1;
                if (decoded.charAt(lastChar) == '\n') {
                    decoded.limit(lastChar);
                }

                resolved = parseContact(decoded.toString());
            } else {
                //Unexpected: No output, but the handler is still running
                resolved = UNRESOLVED;
            }

            out.offer(resolved);
        }

        @Nonnull
        public Person resolve(@Nonnull Person person) throws InterruptedException {
            if (running) {
                //Write contact details for the provided Person to stdin
                process.writeStdin(UTF_8.encode(toContact(person)));

                do {
                    Person resolved = out.poll(250L, TimeUnit.MILLISECONDS);
                    if (resolved == null) {
                        continue;
                    }

                    return resolved == UNRESOLVED ? person : resolved;
                } while (running);

                throw new IllegalStateException("The resolver has terminated unexpectedly");
            } else {
                throw new IllegalStateException("The resolver has already been closed");
            }
        }

        private static Person parseContact(String contact) {
            String name;
            String email;

            int emailSplit = contact.indexOf(" <");
            if (emailSplit == -1) {
                // no email provided
                name = contact.trim();
                email = null;
            } else {
                name = contact.substring(0, emailSplit);
                email = stripEnd(contact.substring(emailSplit + 2), "> ");
            }

            return new Person(name, email);
        }

        private static String toContact(Person person) {
            return person.getName() + " <" + defaultString(person.getEmail()) + ">\n";
        }
    }
}