package com.atlassian.bitbucket.mesh.event;

import com.atlassian.bitbucket.mesh.transaction.Transaction;
import com.atlassian.bitbucket.mesh.transaction.TransactionManager;
import com.atlassian.bitbucket.mesh.transaction.TransactionListener;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.annotation.AnnotationUtils;

import javax.annotation.Nonnull;

import static com.atlassian.bitbucket.mesh.transaction.TransactionState.COMMITTED;

public class SpringEventPublisher implements EventPublisher {

    private final ApplicationEventPublisher applicationEventPublisher;
    private final TransactionManager transactionManager;

    public SpringEventPublisher(ApplicationEventPublisher applicationEventPublisher,
                                TransactionManager transactionManager) {
        this.applicationEventPublisher = applicationEventPublisher;
        this.transactionManager = transactionManager;
    }

    @Override
    public void publish(@Nonnull Object event) {
        //If this event is transaction-aware (typically meaning it includes data that is somehow related to a
        //transaction) and we're in a transaction, don't actually raise the event until the desired publishing point
        //(after commit, or after completion)
        TransactionAware.When when = getTransactionConfiguration(event.getClass());
        if (when == TransactionAware.When.IMMEDIATE ||
                !transactionManager.addListener(new PublishingTransactionListener(event, when))) {

            //Either event publishing should not be deferred, or there is no active transaction. Publish immediately
            applicationEventPublisher.publishEvent(event);
        }
    }

    private static TransactionAware.When getTransactionConfiguration(Class<?> eventClass) {
        TransactionAware annotation = AnnotationUtils.findAnnotation(eventClass, TransactionAware.class);
        if (annotation != null) {
            return annotation.value();
        }
        return TransactionAware.When.IMMEDIATE;
    }

    private class PublishingTransactionListener implements TransactionListener {

        private final Object event;
        private final TransactionAware.When when;

        PublishingTransactionListener(Object event, TransactionAware.When when) {
            this.event = event;
            this.when = when;
        }

        @Override
        public void afterCompletion(@Nonnull Transaction transaction) {
            if (when == TransactionAware.When.AFTER_COMPLETION || transaction.getState() == COMMITTED) {
                applicationEventPublisher.publishEvent(event);
            }
        }
    }
}
