package com.atlassian.bitbucket.mesh.io;

import com.google.common.base.Throwables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;

import static java.util.Objects.requireNonNull;

/**
 * A wrapper around a {@link WritableByteChannel} that supports {@link #addListener listeners} that are run
 * immediately after the channel is {@link #close() closed}.
 */
public class ListenableWritableByteChannel implements WritableByteChannel {

    private static final Logger log = LoggerFactory.getLogger(ListenableWritableByteChannel.class);

    private final WritableByteChannel delegate;
    private final List<Runnable> listeners;
    private final Object lock;

    private volatile boolean closed;

    public ListenableWritableByteChannel(WritableByteChannel delegate) {
        this.delegate = delegate;

        listeners = new ArrayList<>();
        lock = new Object();
    }

    public void addListener(@Nonnull Runnable listener) {
        requireNonNull(listener, "listener");

        synchronized (lock) {
            if (closed) {
                listener.run();
            } else {
                listeners.add(listener);
            }
        }
    }

    @Override
    public void close() throws IOException {
        Exception exception = null;
        try {
            delegate.close();
        } catch (IOException e) {
            exception = e;
        } finally {
            try {
                maybeInvokeListeners();
            } catch (RuntimeException e) {
                if (exception == null) {
                    exception = e;
                } else {
                    exception.addSuppressed(e);
                }
            }
            closed = true;
        }
        if (exception != null) {
            Throwables.throwIfInstanceOf(exception, IOException.class);
            Throwables.throwIfUnchecked(exception);
        }
    }

    @Override
    public boolean isOpen() {
        return delegate.isOpen();
    }

    @Override
    public int write(ByteBuffer src) throws IOException {
        return delegate.write(src);
    }

    private void maybeInvokeListeners() {
        synchronized (lock) {
            if (closed) {
                return;
            }

            RuntimeException exception = null;
            for (Runnable listener : listeners) {
                try {
                    listener.run();
                } catch (RuntimeException e) {
                    log.warn("Listener {} failed", listener.getClass().getName(), e);
                    if (exception == null) {
                        exception = e;
                    } else {
                        exception.addSuppressed(e);
                    }
                }
            }

            if (exception != null) {
                throw exception;
            }
        }
    }
}
