package com.atlassian.bitbucket.mesh.test;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import javax.annotation.Nonnull;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

import static java.util.Objects.requireNonNull;

public class EventualMatcher<T> extends TypeSafeDiagnosingMatcher<T> {

    private final Instant deadline;
    private final Matcher<? super T> delegate;

    public EventualMatcher(@Nonnull Matcher<? super T> delegate, @Nonnull Instant deadline) {
        this.deadline = requireNonNull(deadline, "deadline");
        this.delegate = requireNonNull(delegate, "delegate");
    }

    public EventualMatcher(@Nonnull Matcher<? super T> delegate, @Nonnull Duration timeout) {
        this(delegate, Instant.now().plus(timeout));
    }

    @Nonnull
    public static <T> EventualMatcher<T> eventually(@Nonnull Matcher<? super T> delegate, @Nonnull Instant deadline) {
        return new EventualMatcher<T>(delegate, deadline);
    }

    @Nonnull
    public static <T> EventualMatcher<T> eventually(@Nonnull Matcher<? super T> delegate, @Nonnull Duration timeout) {
        return new EventualMatcher<T>(delegate, timeout);
    }

    @Override
    public void describeTo(Description description) {
        delegate.describeTo(description);
    }

    @Override
    protected boolean matchesSafely(T actual, Description mismatchDescription) {
        do {
            if (delegate.matches(actual)) {
                return true;
            }
            long remaining = Math.max(0L, Duration.between(Instant.now(), deadline).toMillis());
            LockSupport.parkNanos(Math.min(remaining, TimeUnit.MILLISECONDS.toNanos(5L)));
        } while (Instant.now().isBefore(deadline));

        delegate.describeMismatch(actual, mismatchDescription);

        return false;
    }
}
