/*
 * Copyright (C) 2020 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.view;

import static android.os.Trace.TRACE_TAG_GRAPHICS;

import static java.util.Objects.requireNonNull;

import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.ICancellationSignal;
import android.os.RemoteException;
import android.os.Trace;
import android.util.CloseGuard;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import java.lang.ref.Reference;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

/**
 * Mediator between a selected scroll capture target view and a remote process.
 * <p>
 * An instance is created to wrap the selected {@link ScrollCaptureCallback}.
 *
 * @hide
 */
public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub implements
        IBinder.DeathRecipient {

    private static final String TAG = "ScrollCaptureConnection";
    private static final String TRACE_TRACK = "Scroll Capture";
    private static final String START_CAPTURE = "startCapture";
    private static final String REQUEST_IMAGE = "requestImage";

    private static final String END_CAPTURE = "endCapture";
    private static final String SESSION = "Session";

    private final Object mLock = new Object();
    private final Rect mScrollBounds;
    private final Point mPositionInWindow;
    private final Executor mUiThread;
    private final CloseGuard mCloseGuard = new CloseGuard();

    @Nullable
    private ScrollCaptureCallback mLocal;
    @Nullable
    private IScrollCaptureCallbacks mRemote;
    @Nullable
    private ScrollCaptureSession mSession;
    @Nullable
    private CancellationSignal mCancellation;

    private volatile boolean mActive;
    private volatile boolean mConnected;
    private int mTraceId;

    /**
     * Constructs a ScrollCaptureConnection.
     *
     * @param uiThread an executor for the UI thread of the containing View
     * @param selectedTarget  the target the client is controlling
     *
     * @hide
     */
    public ScrollCaptureConnection(
            @NonNull Executor uiThread,
            @NonNull ScrollCaptureTarget selectedTarget) {
        mUiThread = requireNonNull(uiThread, "<uiThread> must non-null");
        requireNonNull(selectedTarget, "<selectedTarget> must non-null");
        mScrollBounds = requireNonNull(Rect.copyOrNull(selectedTarget.getScrollBounds()),
                "target.getScrollBounds() must be non-null to construct a client");
        mLocal = selectedTarget.getCallback();
        mPositionInWindow = new Point(selectedTarget.getPositionInWindow());
    }

    @AnyThread
    @Override
    public ICancellationSignal startCapture(@NonNull Surface surface,
            @NonNull IScrollCaptureCallbacks remote) throws RemoteException {
        mTraceId = System.identityHashCode(surface);
        Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, SESSION, mTraceId);
        Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, START_CAPTURE, mTraceId);
        mCloseGuard.open("ScrollCaptureConnection.close");

        if (!surface.isValid()) {
            throw new RemoteException(new IllegalArgumentException("surface must be valid"));
        }
        mRemote = requireNonNull(remote, "<callbacks> must non-null");
        mRemote.asBinder().linkToDeath(this, 0);
        mConnected = true;

        ICancellationSignal cancellation = CancellationSignal.createTransport();
        mCancellation = CancellationSignal.fromTransport(cancellation);
        mSession = new ScrollCaptureSession(surface, mScrollBounds, mPositionInWindow);

        Runnable listener =
                SafeCallback.create(mCancellation, mUiThread, this::onStartCaptureCompleted);
        // -> UiThread
        mUiThread.execute(() -> {
            if (mLocal != null && mCancellation != null) {
                mLocal.onScrollCaptureStart(mSession, mCancellation, listener);
            }
        });
        return cancellation;
    }

    @UiThread
    private void onStartCaptureCompleted() {
        mActive = true;
        try {
            if (mRemote != null) {
                mRemote.onCaptureStarted();
            } else {
                close();
            }
        } catch (RemoteException e) {
            Log.w(TAG, "Shutting down due to error: ", e);
            close();
        }
        mCancellation = null;
        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
    }

    @AnyThread
    @Override
    public ICancellationSignal requestImage(Rect requestRect) throws RemoteException {
        Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, REQUEST_IMAGE, mTraceId);
        checkActive();
        cancelPendingAction();
        ICancellationSignal cancellation = CancellationSignal.createTransport();
        mCancellation = CancellationSignal.fromTransport(cancellation);

        Consumer<Rect> listener =
                SafeCallback.create(mCancellation, mUiThread, this::onImageRequestCompleted);
        // -> UiThread
        mUiThread.execute(() -> {
            if (mLocal != null && mSession != null && mCancellation != null) {
                mLocal.onScrollCaptureImageRequest(
                        mSession, mCancellation, new Rect(requestRect), listener);
            }
        });

        return cancellation;
    }

    @UiThread
    void onImageRequestCompleted(Rect capturedArea) {
        try {
            if (mRemote != null) {
                mRemote.onImageRequestCompleted(0, capturedArea);
            } else {
                close();
            }
        } catch (RemoteException e) {
            Log.w(TAG, "Shutting down due to error: ", e);
            close();
        } finally {
            mCancellation = null;
        }
        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
    }

    @AnyThread
    @Override
    public ICancellationSignal endCapture() throws RemoteException {
        Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, END_CAPTURE, mTraceId);
        checkActive();
        cancelPendingAction();
        ICancellationSignal cancellation = CancellationSignal.createTransport();
        mCancellation = CancellationSignal.fromTransport(cancellation);

        Runnable listener =
                SafeCallback.create(mCancellation, mUiThread, this::onEndCaptureCompleted);
        // -> UiThread
        mUiThread.execute(() -> {
            if (mLocal != null) {
                mLocal.onScrollCaptureEnd(listener);
            }
        });
        return cancellation;
    }

    @UiThread
    private void onEndCaptureCompleted() {
        mActive = false;
        try {
            if (mRemote != null) {
                mRemote.onCaptureEnded();
            }
        } catch (RemoteException e) {
            Log.w(TAG, "Caught exception confirming capture end!", e);
        } finally {
            mCancellation = null;
            close();
        }
        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
    }

    @Override
    public void binderDied() {
        Trace.instantForTrack(TRACE_TAG_GRAPHICS, TRACE_TRACK, "binderDied");
        Log.e(TAG, "Controlling process just died.");
        close();

    }

    @AnyThread
    @Override
    public synchronized void close() {
        Trace.instantForTrack(TRACE_TAG_GRAPHICS, TRACE_TRACK, "close");
        if (mActive) {
            Log.w(TAG, "close(): capture session still active! Ending now.");
            cancelPendingAction();
            final ScrollCaptureCallback callback = mLocal;
            mUiThread.execute(() -> {
                if (callback != null) {
                    callback.onScrollCaptureEnd(() -> { /* ignore */ });
                }
            });
            mActive = false;
        }
        if (mRemote != null) {
            mRemote.asBinder().unlinkToDeath(this, 0);
        }
        mActive = false;
        mConnected = false;
        mSession = null;
        mRemote = null;
        mLocal = null;
        mCloseGuard.close();
        Trace.endSection();
        Reference.reachabilityFence(this);
    }

    private void cancelPendingAction() {
        if (mCancellation != null) {
            Trace.instantForTrack(TRACE_TAG_GRAPHICS, TRACE_TRACK, "CancellationSignal.cancel");
            Log.w(TAG, "cancelling pending operation.");
            mCancellation.cancel();
            mCancellation = null;
        }
    }

    @VisibleForTesting
    public boolean isConnected() {
        return mConnected;
    }

    @VisibleForTesting
    public boolean isActive() {
        return mActive;
    }

    private void checkActive() throws RemoteException {
        synchronized (mLock) {
            if (!mActive) {
                throw new RemoteException(new IllegalStateException("Not started!"));
            }
        }
    }

    /** @return a string representation of the state of this client */
    public String toString() {
        return "ScrollCaptureConnection{"
                + "active=" + mActive
                + ", session=" + mSession
                + ", remote=" + mRemote
                + ", local=" + mLocal
                + "}";
    }

    protected void finalize() throws Throwable {
        try {
            mCloseGuard.warnIfOpen();
            close();
        } finally {
            super.finalize();
        }
    }

    private static class SafeCallback<T> {
        private final CancellationSignal mSignal;
        private final Executor mExecutor;
        private final AtomicReference<T> mValue;

        protected SafeCallback(CancellationSignal signal, Executor executor, T value) {
            mSignal = signal;
            mValue = new AtomicReference<>(value);
            mExecutor = executor;
        }

        // Provide the value to the consumer to accept only once.
        protected final void maybeAccept(Consumer<T> consumer) {
            T value = mValue.getAndSet(null);
            if (mSignal.isCanceled()) {
                Log.w(TAG, "callback ignored, operation already cancelled");
                return;
            }
            if (value != null) {
                mExecutor.execute(() -> consumer.accept(value));
            } else {
                Log.w(TAG, "callback ignored, value already delivered");
            }
        }

        static Runnable create(CancellationSignal signal, Executor executor, Runnable target) {
            return new RunnableCallback(signal, executor, target);
        }

        static <T> Consumer<T> create(CancellationSignal signal, Executor executor,
                Consumer<T> target) {
            return new ConsumerCallback<>(signal, executor, target);
        }
    }

    private static final class RunnableCallback extends SafeCallback<Runnable> implements Runnable {
        RunnableCallback(CancellationSignal signal, Executor executor, Runnable target) {
            super(signal, executor, target);
        }

        @Override
        public void run() {
            maybeAccept(Runnable::run);
        }
    }

    private static final class ConsumerCallback<T> extends SafeCallback<Consumer<T>>
            implements Consumer<T> {
        ConsumerCallback(CancellationSignal signal, Executor executor, Consumer<T> target) {
            super(signal, executor, target);
        }

        @Override
        public void accept(T value) {
            maybeAccept((target) -> target.accept(value));
        }
    }
}
