package com.atlassian.bitbucket.mesh.git.builder.catfile;

import com.atlassian.bitbucket.mesh.git.GitObjectType;
import com.atlassian.bitbucket.mesh.git.mailmap.MailmapResolver;
import com.atlassian.bitbucket.mesh.git.ref.AnnotatedTag;
import com.atlassian.bitbucket.mesh.git.ref.AnnotatedTagCallback;
import com.atlassian.bitbucket.mesh.process.handler.LineDecoder;
import com.atlassian.bitbucket.mesh.process.handler.LineSink;
import com.atlassian.bitbucket.mesh.util.Person;

import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Iterator;
import java.util.Set;

import static com.atlassian.bitbucket.mesh.git.GitRefPattern.TAGS;
import static com.atlassian.bitbucket.mesh.git.ref.StreamAnnotatedTagsParameters.UNLIMITED_MESSAGE_LENGTH;
import static java.util.Objects.requireNonNull;

public class CallbackAnnotatedTagBatchCatFileStdioHandler extends AbstractBatchCatFileStdioHandler<Void> {

    private final AnnotatedTagCallback callback;
    private final MailmapResolver mailmapResolver;
    private final int maxMessageLength;
    private final Iterator<String> tagIds;

    private AnnotatedTagParser tagParser;

    public CallbackAnnotatedTagBatchCatFileStdioHandler(@Nonnull Set<String> tagIds, int maxMessageLength,
                                                        @Nonnull MailmapResolver mailmapResolver,
                                                        @Nonnull AnnotatedTagCallback callback) {
        this.callback = requireNonNull(callback, "callback");
        this.mailmapResolver = requireNonNull(mailmapResolver, "mailmapResolver");
        this.maxMessageLength = maxMessageLength;
        this.tagIds = requireNonNull(tagIds, "tagIds").iterator();
    }

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

    @Override
    protected String getNextObjectId() {
        return !isCanceled() && tagIds.hasNext() ? tagIds.next() : null;
    }

    @Override
    protected boolean onContent(@Nonnull ByteBuffer content, boolean closed) {
        if (tagParser.pump(content, closed)) {
            //The tag is complete, or we've reached the max message length
            AnnotatedTag tag = tagParser.build(mailmapResolver);
            if (tag != null) {
                callback.onTag(tag);
            }

            //If there are any more bytes for this tag, we're not interested in them
            return false;
        }

        if (closed) {
            tagParser = null;
        }

        return true;
    }

    @Override
    protected void onMissing(@Nonnull String objectId) {
        callback.onMissing(objectId);
    }

    @Override
    protected boolean onObject(@Nonnull BatchHeader header) {
        String objectType = header.getType();
        if ("tag".equals(objectType)) {
            //For tags, we need to read the object's bytes and parse them. Prepare a new parser
            tagParser = new AnnotatedTagParser(header.getObjectId(), maxMessageLength);
            return true;
        }

        //For any other object type, notify the callback but don't parse the object
        callback.onNonTag(header.getObjectId(), GitObjectType.fromObjectType(header.getType()));
        return false;
    }

    private static class AnnotatedTagParser implements LineSink {

        private final LineDecoder pump;
        private final String tagHash;

        private StringBuilder message;
        private String objectId;
        private int remaining;
        private String tagId;
        private Person tagger;
        private Instant taggerTimestamp;

        AnnotatedTagParser(String tagHash, int maxMessageLength) {
            this.tagHash = tagHash;

            //Each tag's bytes must be treated like a separate output stream and use their own pump
            pump = new LineDecoder(StandardCharsets.UTF_8, LineDecoder.Mode.LF, Math.max(1024, maxMessageLength));
            remaining = maxMessageLength == UNLIMITED_MESSAGE_LENGTH ? Integer.MAX_VALUE : maxMessageLength;
        }

        @Override
        public boolean accept(@Nonnull String line, boolean truncated) {
            if (message != null) {
                if (line.startsWith("-----BEGIN PGP SIGNATURE-----")) {
                    //The signature is not considered part of the tag's message, so if we find one
                    //we're done reading the message (and, by extension, the tag)
                    return false;
                }

                int lineLength = line.length();
                if (remaining < lineLength) {
                    message.append(line, 0, remaining);
                    remaining = 0;
                } else {
                    message.append(line);
                    remaining -= lineLength;
                }
                message.append('\n');

                return remaining > 0;
            }

            if (line.startsWith("object ")) {
                objectId = line.substring(7);
            } else if (line.startsWith("tag ")) {
                tagId = line.substring(4);
            } else if (line.startsWith("tagger ")) {
                String taggerSubstring = line.substring(7);
                tagger = AnnotatedTag.parseTagger(taggerSubstring);
                taggerTimestamp = AnnotatedTag.parseTaggerTimestamp(taggerSubstring);
            } else if (line.trim().isEmpty()) {
                //The first blank line separates the tag's metadata from its message
                message = new StringBuilder();
            }

            return true;
        }

        AnnotatedTag build(MailmapResolver mailmapResolver) {
            if (objectId == null || tagId == null || tagger == null) {
                return null;
            }

            return new AnnotatedTag.Builder()
                    .id(TAGS.qualify(tagId))
                    .hash(tagHash)
                    .latestCommit(objectId)
                    .message(message == null ? "" : message.toString())
                    .tagger(mailmapResolver.resolve(tagger))
                    .taggerTimestamp(taggerTimestamp)
                    .build();
        }

        boolean pump(ByteBuffer buffer, boolean closed) {
            return pump.forEach(buffer, closed, this);
        }
    }
}