/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package android.testing;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.TestLooperManager;
import android.util.ArrayMap;

import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.runners.model.FrameworkMethod;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * This is a wrapper around {@link TestLooperManager} to make it easier to manage
 * and provide an easy annotation for use with tests.
 *
 * @see TestableLooperTest TestableLooperTest for examples.
 *
 * @deprecated Use {@link android.os.TestLooperManager} or {@link
 *     org.robolectric.shadows.ShadowLooper} instead.
 *     This class is not actively maintained.
 *     Both of the recommended alternatives allow fine control of execution.
 *     The Robolectric class also allows advancing time.
 */
public class TestableLooper {

    /**
     * Whether to hold onto the main thread through all tests in an attempt to
     * catch crashes.
     */
    public static final boolean HOLD_MAIN_THREAD = false;
    private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
    private static final Field MESSAGE_NEXT_FIELD;
    private static final Field MESSAGE_WHEN_FIELD;

    private Looper mLooper;
    private MessageQueue mQueue;
    private MessageHandler mMessageHandler;

    private Handler mHandler;
    private TestLooperManager mQueueWrapper;

    /**
     * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
     */
    private static boolean isAtLeastBaklava() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
    }

    static {
        if (isAtLeastBaklava()) {
            MESSAGE_QUEUE_MESSAGES_FIELD = null;
            MESSAGE_NEXT_FIELD = null;
            MESSAGE_WHEN_FIELD = null;
        } else {
            try {
                MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
                MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
                MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
                MESSAGE_NEXT_FIELD.setAccessible(true);
                MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
                MESSAGE_WHEN_FIELD.setAccessible(true);
            } catch (NoSuchFieldException e) {
                throw new RuntimeException("Failed to initialize TestableLooper", e);
            }
        }
    }

    public TestableLooper(Looper l) throws Exception {
        this(acquireLooperManager(l), l);
    }

    private TestableLooper(TestLooperManager wrapper, Looper l) {
        mQueueWrapper = Objects.requireNonNull(wrapper);
        setupQueue(l);
    }

    private TestableLooper(Looper looper, boolean b) {
        setupQueue(looper);
    }

    /**
     * Wrap the given runnable so that it will run blocking on the Looper that will be set up for
     * the given test.
     * <p>
     * This method is required to support any TestRule which needs to run setup and/or teardown code
     * on the TestableLooper. Whether using {@link AndroidTestingRunner} or
     * {@link TestWithLooperRule}, the TestRule's Statement evaluates on the test instrumentation
     * thread, rather than the TestableLooper thread, so access to the TestableLooper is required.
     * However, {@link #get(Object)} will return {@code null} both before and after the inner
     * statement is evaluated:
     * <ul>
     * <li>Before the test {@link #get} returns {@code null} because while the TestableLooperHolder
     * is accessible in sLoopers, it has not been initialized with an actual TestableLooper yet.
     * This method's use of the internal LooperFrameworkMethod ensures that all setup and teardown
     * of the TestableLooper happen as it would for all other wrapped code blocks.
     * <li>After the test {@link #get} can return {@code null} because many tests call
     * {@link #remove} in the teardown method. The fact that this method returns a runnable allows
     * it to be called before the test (when the TestableLooperHolder is still in sLoopers), and
     * then executed as teardown after the test.
     * </ul>
     *
     * @param test     the test instance (just like passed to {@link #get(Object)})
     * @param runnable the operation that should eventually be run on the TestableLooper
     * @return a runnable that will block the thread on which it is called until the given runnable
     *          is finished.  Will be {@code null} if there is no looper for the given test.
     * @hide
     */
    @Nullable
    public static RunnableWithException wrapWithRunBlocking(
            Object test, @NonNull RunnableWithException runnable) {
        TestableLooperHolder looperHolder = sLoopers.get(test);
        if (looperHolder == null) {
            return null;
        }
        try {
            FrameworkMethod base = new FrameworkMethod(runnable.getClass().getMethod("run"));
            LooperFrameworkMethod wrapped = new LooperFrameworkMethod(base, looperHolder);
            return () -> {
                try {
                    wrapped.invokeExplosively(runnable);
                } catch (RuntimeException | Error e) {
                    throw e;
                } catch (Throwable e) {
                    throw new RuntimeException(e);
                }
            };
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    public Looper getLooper() {
        return mLooper;
    }

    private void setupQueue(Looper l) {
        mLooper = l;
        mQueue = mLooper.getQueue();
        mHandler = new Handler(mLooper);
    }

    /**
     * Must be called to release the looper when the test is complete, otherwise
     * the looper will not be available for any subsequent tests. This is
     * automatically handled for tests using {@link RunWithLooper}.
     */
    public void destroy() {
        mQueueWrapper.release();
        if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
            TestableInstrumentation.releaseMain();
        }
    }

    /**
     * Sets a callback for all messages processed on this TestableLooper.
     *
     * @see {@link MessageHandler}
     */
    public void setMessageHandler(MessageHandler handler) {
        mMessageHandler = handler;
    }

    /**
     * Parse num messages from the message queue.
     *
     * @param num Number of messages to parse
     */
    public int processMessages(int num) {
        return processMessagesInternal(num, null);
    }

    private int processMessagesInternal(int num, Runnable barrierRunnable) {
        for (int i = 0; i < num; i++) {
            if (!processSingleMessage(barrierRunnable)) {
                return i + 1;
            }
        }
        return num;
    }

    /**
     * Process up to a certain number of messages, not blocking if the queue has less messages than
     * that
     * @param num the maximum number of messages to process
     * @return the number of messages processed. This will be at most {@code num}.
     */

    public int processMessagesNonBlocking(int num) {
        final AtomicBoolean reachedBarrier = new AtomicBoolean(false);
        Runnable barrierRunnable = () -> {
            reachedBarrier.set(true);
        };
        mHandler.post(barrierRunnable);
        waitForMessage(mQueueWrapper, mHandler, barrierRunnable);
        try {
            return processMessagesInternal(num, barrierRunnable) + (reachedBarrier.get() ? -1 : 0);
        } finally {
            mHandler.removeCallbacks(barrierRunnable);
        }
    }

    /**
     * Process messages in the queue until no more are found.
     */
    public void processAllMessages() {
        while (processQueuedMessages() != 0) ;
    }

    public long peekWhen() {
        if (isAtLeastBaklava()) {
            return peekWhenBaklava();
        } else {
            return peekWhenLegacy();
        }
    }

    private long peekWhenBaklava() {
        Long when = mQueueWrapper.peekWhen();
        if (when != null) {
            return when;
        } else {
            return 0;
        }
    }

    private long peekWhenLegacy() {
        try {
            Message msg = (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(mLooper.getQueue());
            if (msg != null) {
                return msg.getWhen();
            } else {
                return 0;
            }
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e);
        }
    }

    public void moveTimeForward(long milliSeconds) {
        if (isAtLeastBaklava()) {
            moveTimeForwardBaklava(milliSeconds);
        } else {
            moveTimeForwardLegacy(milliSeconds);
        }
    }

    private void moveTimeForwardBaklava(long milliSeconds) {
        // Drain all Messages from the queue.
        Queue<Message> messages = new ArrayDeque<>();
        while (true) {
            Message message = mQueueWrapper.poll();
            if (message == null) {
                break;
            }

            // Adjust the Message's delivery time.
            long newWhen = message.when - milliSeconds;
            if (newWhen < 0) {
                newWhen = 0;
            }
            message.when = newWhen;
            messages.add(message);
        }

        // Repost all Messages back to the queue with a new time.
        while (true) {
            Message message = messages.poll();
            if (message == null) {
                break;
            }

            Runnable callback = message.getCallback();
            Handler handler = message.getTarget();
            long when = message.getWhen();

            // The Message cannot be re-enqueued because it is marked in use.
            // Make a copy of the Message and recycle the original.
            // This resets {@link Message#isInUse()} but retains all other content.
            {
                Message newMessage = Message.obtain();
                newMessage.copyFrom(message);
                newMessage.setCallback(callback);
                mQueueWrapper.recycle(message);
                message = newMessage;
            }

            // Send the Message back to its Handler to be re-enqueued.
            handler.sendMessageAtTime(message, when);
        }
    }

    private void moveTimeForwardLegacy(long milliSeconds) {
        try {
            Message msg = (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(mLooper.getQueue());
            while (msg != null) {
                long updatedWhen = msg.getWhen() - milliSeconds;
                if (updatedWhen < 0) {
                    updatedWhen = 0;
                }
                MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
                msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
            }
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e);
        }
    }

    private int processQueuedMessages() {
        int count = 0;
        Runnable barrierRunnable = () -> { };
        mHandler.post(barrierRunnable);
        waitForMessage(mQueueWrapper, mHandler, barrierRunnable);
        while (processSingleMessage(barrierRunnable)) count++;
        return count;
    }

    private boolean processSingleMessage(Runnable barrierRunnable) {
        try {
            Message result = mQueueWrapper.next();
            if (result != null) {
                // This is a break message.
                if (result.getCallback() == barrierRunnable) {
                    mQueueWrapper.execute(result);
                    mQueueWrapper.recycle(result);
                    return false;
                }

                if (mMessageHandler != null) {
                    if (mMessageHandler.onMessageHandled(result)) {
                        mQueueWrapper.execute(result);
                        mQueueWrapper.recycle(result);
                    } else {
                        mQueueWrapper.recycle(result);
                        // Message handler indicated it doesn't want us to continue.
                        return false;
                    }
                } else {
                    mQueueWrapper.execute(result);
                    mQueueWrapper.recycle(result);
                }
            } else {
                // No messages, don't continue parsing
                return false;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return true;
    }

    /**
     * Runs an executable with myLooper set and processes all messages added.
     */
    public void runWithLooper(RunnableWithException runnable) throws Exception {
        new Handler(getLooper()).post(() -> {
            try {
                runnable.run();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        processAllMessages();
    }

    public interface RunnableWithException {
        void run() throws Exception;
    }

    /**
     * Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and
     * run this test/class on that thread. The {@link TestableLooper} can be acquired using
     * {@link #get(Object)}.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD, ElementType.TYPE})
    public @interface RunWithLooper {
        boolean setAsMainLooper() default false;
    }

    private static void waitForMessage(TestLooperManager queueWrapper, Handler handler,
            Runnable execute) {
        for (int i = 0; i < 10; i++) {
            if (!queueWrapper.hasMessages(handler, null, execute)) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                }
            }
        }
        if (!queueWrapper.hasMessages(handler, null, execute)) {
            throw new RuntimeException("Message didn't queue...");
        }
    }

    private static TestLooperManager acquireLooperManager(Looper l) {
        if (HOLD_MAIN_THREAD && l == Looper.getMainLooper()) {
            TestableInstrumentation.acquireMain();
        }
        return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l);
    }

    private static final Map<Object, TestableLooperHolder> sLoopers = new ArrayMap<>();

    /**
     * For use with {@link RunWithLooper}, used to get the TestableLooper that was
     * automatically created for this test.
     */
    public static TestableLooper get(Object test) {
        final TestableLooperHolder looperHolder = sLoopers.get(test);
        return (looperHolder != null) ? looperHolder.mTestableLooper : null;
    }

    public static void remove(Object test) {
        sLoopers.remove(test);
    }

    /**
     * Holder object that contains {@link TestableLooper} so that its initialization can be
     * deferred until a test case is actually run, instead of forcing it to be created at
     * {@link FrameworkMethod} construction time.
     *
     * This deferral is important because some test environments may configure
     * {@link Looper#getMainLooper()} as part of a {@code Rule} instead of assuming it's globally
     * initialized and unconditionally available.
     */
    private static class TestableLooperHolder {
        private final boolean mSetAsMain;
        private final Object mTest;

        private TestableLooper mTestableLooper;
        private Looper mLooper;
        private Handler mHandler;
        private HandlerThread mHandlerThread;

        public TestableLooperHolder(boolean setAsMain, Object test) {
            mSetAsMain = setAsMain;
            mTest = test;
        }

        public void ensureInit() {
            if (mLooper != null) return;
            try {
                mLooper = mSetAsMain ? Looper.getMainLooper() : createLooper();
                mTestableLooper = new TestableLooper(mLooper, false);
                if (!mSetAsMain) {
                    mTestableLooper.getLooper().getThread().setName(mTest.getClass().getName());
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            mHandler = new Handler(mLooper);
        }

        private Looper createLooper() {
            // TODO: Find way to share these.
            mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
            mHandlerThread.start();
            return mHandlerThread.getLooper();
        }
    }

    static class LooperFrameworkMethod extends FrameworkMethod {
        private TestableLooperHolder mLooperHolder;

        public LooperFrameworkMethod(FrameworkMethod base, TestableLooperHolder looperHolder) {
            super(base.getMethod());
            mLooperHolder = looperHolder;
        }

        public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) {
            TestableLooperHolder looperHolder = sLoopers.get(test);
            if (looperHolder == null) {
                looperHolder = new TestableLooperHolder(setAsMain, test);
                sLoopers.put(test, looperHolder);
            }
            return new LooperFrameworkMethod(base, looperHolder);
        }

        @Override
        public Object invokeExplosively(Object target, Object... params) throws Throwable {
            mLooperHolder.ensureInit();
            if (Looper.myLooper() == mLooperHolder.mLooper) {
                // Already on the right thread from another statement, just execute then.
                return super.invokeExplosively(target, params);
            }
            boolean set = mLooperHolder.mTestableLooper.mQueueWrapper == null;
            if (set) {
                mLooperHolder.mTestableLooper.mQueueWrapper = acquireLooperManager(
                        mLooperHolder.mLooper);
            }
            try {
                Object[] ret = new Object[1];
                // Run the execution on the looper thread.
                Runnable execute = () -> {
                    try {
                        ret[0] = super.invokeExplosively(target, params);
                    } catch (Throwable throwable) {
                        throw new LooperException(throwable);
                    }
                };
                Message m = Message.obtain(mLooperHolder.mHandler, execute);

                // Dispatch our message.
                try {
                    mLooperHolder.mTestableLooper.mQueueWrapper.execute(m);
                } catch (LooperException e) {
                    throw e.getSource();
                } catch (RuntimeException re) {
                    // If the TestLooperManager has to post, it will wrap what it throws in a
                    // RuntimeException, make sure we grab the actual source.
                    if (re.getCause() instanceof LooperException) {
                        throw ((LooperException) re.getCause()).getSource();
                    } else {
                        throw re.getCause();
                    }
                } finally {
                    m.recycle();
                }
                return ret[0];
            } finally {
                if (set) {
                    mLooperHolder.mTestableLooper.mQueueWrapper.release();
                    mLooperHolder.mTestableLooper.mQueueWrapper = null;
                    if (HOLD_MAIN_THREAD && mLooperHolder.mLooper == Looper.getMainLooper()) {
                        TestableInstrumentation.releaseMain();
                    }
                }
            }
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            if (mLooperHolder.mHandlerThread != null) {
                mLooperHolder.mHandlerThread.quit();
            }
        }

        private static class LooperException extends RuntimeException {
            private final Throwable mSource;

            public LooperException(Throwable t) {
                mSource = t;
            }

            public Throwable getSource() {
                return mSource;
            }
        }
    }

    /**
     * Callback to control the execution of messages on the looper, when set with
     * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)}
     * will get called back for every message processed on the {@link TestableLooper}.
     */
    public interface MessageHandler {
        /**
         * Return true to have the message executed and delivered to target.
         * Return false to not execute the message and stop executing messages.
         */
        boolean onMessageHandled(Message m);
    }
}
