/*
 * Copyright (C) 2018 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.service.autofill.augmented;

import static android.service.autofill.augmented.AugmentedAutofillService.sDebug;
import static android.service.autofill.augmented.AugmentedAutofillService.sVerbose;

import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
import android.service.autofill.augmented.PresentationParams.Area;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.autofill.IAutofillWindowPresenter;

import com.android.internal.annotations.GuardedBy;

import dalvik.system.CloseGuard;

import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.Objects;

/**
 * Handle to a window used to display the augmented autofill UI.
 *
 * <p>The steps to create an augmented autofill UI are:
 *
 * <ol>
 *   <li>Gets the {@link PresentationParams} from the {@link FillRequest}.
 *   <li>Gets the {@link Area} to display the UI (for example, through
 *   {@link PresentationParams#getSuggestionArea()}.
 *   <li>Creates a {@link View} that must fit in the {@link Area#getBounds() area boundaries}.
 *   <li>Set the proper listeners to the view (for example, a click listener that
 *   triggers {@link FillController#autofill(java.util.List)}
 *   <li>Call {@link #update(Area, View, long)} with these arguments.
 *   <li>Create a {@link FillResponse} with the {@link FillWindow}.
 *   <li>Pass such {@link FillResponse} to {@link FillCallback#onSuccess(FillResponse)}.
 * </ol>
 *
 * @hide
 */
@SystemApi
public final class FillWindow implements AutoCloseable {
    private static final String TAG = FillWindow.class.getSimpleName();

    private final Object mLock = new Object();
    private final CloseGuard mCloseGuard = CloseGuard.get();

    private final @NonNull Handler mUiThreadHandler = new Handler(Looper.getMainLooper());

    @GuardedBy("mLock")
    private @NonNull WindowManager mWm;
    @GuardedBy("mLock")
    private View mFillView;
    @GuardedBy("mLock")
    private boolean mShowing;
    @GuardedBy("mLock")
    private @Nullable Rect mBounds;

    @GuardedBy("mLock")
    private boolean mUpdateCalled;
    @GuardedBy("mLock")
    private boolean mDestroyed;

    private @NonNull AutofillProxy mProxy;

    /**
     * Updates the content of the window.
     *
     * @param rootView new root view
     * @param area coordinates to render the view.
     * @param flags currently not used.
     *
     * @return boolean whether the window was updated or not.
     *
     * @throws IllegalArgumentException if the area is not compatible with this window
     */
    public boolean update(@NonNull Area area, @NonNull View rootView, long flags) {
        if (sDebug) {
            Log.d(TAG, "Updating " + area + " + with " + rootView);
        }
        // TODO(b/123100712): add test case for null
        Objects.requireNonNull(area);
        Objects.requireNonNull(area.proxy);
        Objects.requireNonNull(rootView);
        // TODO(b/123100712): must check the area is a valid object returned by
        // SmartSuggestionParams, throw IAE if not

        final PresentationParams smartSuggestion = area.proxy.getSmartSuggestionParams();
        if (smartSuggestion == null) {
            Log.w(TAG, "No SmartSuggestionParams");
            return false;
        }

        final Rect rect = area.getBounds();
        if (rect == null) {
            Log.wtf(TAG, "No Rect on SmartSuggestionParams");
            return false;
        }

        synchronized (mLock) {
            checkNotDestroyedLocked();

            mProxy = area.proxy;

            // TODO(b/123227534): once we have the SurfaceControl approach, we should update the
            // window instead of destroying. In fact, it might be better to allocate a full window
            // initially, which is transparent (and let touches get through) everywhere but in the
            // rect boundaries.

            // TODO(b/123099468): make sure all touch events are handled, window is always closed,
            // etc.

            mWm = rootView.getContext().getSystemService(WindowManager.class);
            mFillView = rootView;
            // Listen to the touch outside to destroy the window when typing is detected.
            mFillView.setOnTouchListener(
                    (view, motionEvent) -> {
                        if (motionEvent.getAction() == MotionEvent.ACTION_OUTSIDE) {
                            if (sVerbose) Log.v(TAG, "Outside touch detected, hiding the window");
                            hide();
                        }
                        return false;
                    }
            );
            mShowing = false;
            mBounds = new Rect(area.getBounds());
            if (sDebug) {
                Log.d(TAG, "Created FillWindow: params= " + smartSuggestion + " view=" + rootView);
            }
            mUpdateCalled = true;
            mDestroyed = false;
            mProxy.setFillWindow(this);
            return true;
        }
    }

    /** @hide */
    void show() {
        // TODO(b/123100712): check if updated first / throw exception
        if (sDebug) Log.d(TAG, "show()");
        synchronized (mLock) {
            checkNotDestroyedLocked();
            if (mWm == null || mFillView == null) {
                throw new IllegalStateException("update() not called yet, or already destroyed()");
            }
            if (mProxy != null) {
                try {
                    mProxy.requestShowFillUi(mBounds.right - mBounds.left,
                            mBounds.bottom - mBounds.top,
                            /*anchorBounds=*/ null, new FillWindowPresenter(this));
                } catch (RemoteException e) {
                    Log.w(TAG, "Error requesting to show fill window", e);
                }
                mProxy.logEvent(AutofillProxy.REPORT_EVENT_UI_SHOWN);
            }
        }
    }

    /**
     * Hides the window.
     *
     * <p>The window is not destroyed and can be shown again
     */
    private void hide() {
        if (sDebug) Log.d(TAG, "hide()");
        synchronized (mLock) {
            checkNotDestroyedLocked();
            if (mWm == null || mFillView == null) {
                throw new IllegalStateException("update() not called yet, or already destroyed()");
            }
            if (mProxy != null && mShowing) {
                try {
                    mProxy.requestHideFillUi();
                } catch (RemoteException e) {
                    Log.w(TAG, "Error requesting to hide fill window", e);
                }
            }
        }
    }

    private void handleShow(WindowManager.LayoutParams p) {
        if (sDebug) Log.d(TAG, "handleShow()");
        synchronized (mLock) {
            if (mWm != null && mFillView != null) {
                try {
                    p.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
                    if (!mShowing) {
                        mWm.addView(mFillView, p);
                        mShowing = true;
                    } else {
                        mWm.updateViewLayout(mFillView, p);
                    }
                } catch (WindowManager.BadTokenException e) {
                    if (sDebug) Log.d(TAG, "Filed with token " + p.token + " gone.");
                } catch (IllegalStateException e) {
                    if (sDebug) Log.d(TAG, "Exception showing window.");
                }
            }
        }
    }

    private void handleHide() {
        if (sDebug) Log.d(TAG, "handleHide()");
        synchronized (mLock) {
            if (mWm != null && mFillView != null && mShowing) {
                try {
                    mWm.removeView(mFillView);
                    mShowing = false;
                } catch (IllegalStateException e) {
                    if (sDebug) Log.d(TAG, "Exception hiding window.");
                }
            }
        }
    }

    /**
     * Destroys the window.
     *
     * <p>Once destroyed, this window cannot be used anymore
     */
    public void destroy() {
        if (sDebug) {
            Log.d(TAG,
                    "destroy(): mDestroyed=" + mDestroyed + " mShowing=" + mShowing + " mFillView="
                            + mFillView);
        }
        synchronized (mLock) {
            if (mDestroyed) return;
            if (mUpdateCalled) {
                mFillView.setOnClickListener(null);
                hide();
                mProxy.logEvent(AutofillProxy.REPORT_EVENT_UI_DESTROYED);
            }
            mDestroyed = true;
            mCloseGuard.close();
        }
    }

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

    private void checkNotDestroyedLocked() {
        if (mDestroyed) {
            throw new IllegalStateException("already destroyed()");
        }
    }

    /** @hide */
    public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
        synchronized (this) {
            pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
            pw.print(prefix); pw.print("updateCalled: "); pw.println(mUpdateCalled);
            if (mFillView != null) {
                pw.print(prefix); pw.print("fill window: ");
                pw.println(mShowing ? "shown" : "hidden");
                pw.print(prefix); pw.print("fill view: ");
                pw.println(mFillView);
                pw.print(prefix); pw.print("mBounds: ");
                pw.println(mBounds);
                pw.print(prefix); pw.print("mWm: ");
                pw.println(mWm);
            }
        }
    }

    /** @hide */
    @Override
    public void close() {
        destroy();
    }

    private static final class FillWindowPresenter extends IAutofillWindowPresenter.Stub {
        private final @NonNull WeakReference<FillWindow> mFillWindowReference;

        FillWindowPresenter(@NonNull FillWindow fillWindow) {
            mFillWindowReference = new WeakReference<>(fillWindow);
        }

        @Override
        public void show(WindowManager.LayoutParams p, Rect transitionEpicenter,
                boolean fitsSystemWindows, int layoutDirection) {
            if (sDebug) Log.d(TAG, "FillWindowPresenter.show()");
            final FillWindow fillWindow = mFillWindowReference.get();
            if (fillWindow != null) {
                fillWindow.mUiThreadHandler.sendMessage(
                        obtainMessage(FillWindow::handleShow, fillWindow, p));
            }
        }

        @Override
        public void hide(Rect transitionEpicenter) {
            if (sDebug) Log.d(TAG, "FillWindowPresenter.hide()");
            final FillWindow fillWindow = mFillWindowReference.get();
            if (fillWindow != null) {
                fillWindow.mUiThreadHandler.sendMessage(
                        obtainMessage(FillWindow::handleHide, fillWindow));
            }
        }
    }
}
