// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser.input;

import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.view.ActionMode;
import android.view.ActionMode.Callback2;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupWindow;

import org.chromium.base.Log;
import org.chromium.base.PackageUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.SuppressFBWarnings;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * This is a workaround for LG Email app: crbug.com/651706
 * LG Email app runs UI-thread APIs from InputConnection methods. This is not allowable with
 * the change ImeThread introduces, and LG Email app is bundled and cannot be updated without
 * a system update. However, LG Email team is committed to fixing this in the near future.
 * This is a version code limited workaround to avoid crashes in the app.
 */
public final class LGEmailActionModeWorkaround {
    private static final String TAG = "cr_Ime";

    // This is the last broken version shipped on LG V20/NRD90M.
    public static final int LGEmailWorkaroundMaxVersion = 67502100;

    /**
     * Run this workaround only when it's applicable and absolutely necessary.
     * @param context The context
     * @param actionMode The {@ActionMode} to apply the workaround to.
     */
    public static void runIfNecessary(Context context, ActionMode actionMode) {
        if (shouldAllowActionModeDestroyOnNonUiThread(context)) {
            allowActionModeDestroyOnNonUiThread(actionMode);
        }
    }

    private static boolean shouldAllowActionModeDestroyOnNonUiThread(Context context) {
        String appName = context.getPackageName();
        int versionCode = PackageUtils.getPackageVersion(context, appName);
        int appTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
        if (versionCode == -1) return false;

        if (appTargetSdkVersion < Build.VERSION_CODES.M
                || appTargetSdkVersion > Build.VERSION_CODES.N) {
            return false;
        }

        final String lgeMailPackageId = "com.lge.email";
        if (!lgeMailPackageId.equals(appName)) return false;
        if (versionCode > LGEmailWorkaroundMaxVersion) return false;

        Log.w(TAG, "Working around action mode LG Email bug in WebView (http://crbug.com/651706). "
                + "APK name: " + lgeMailPackageId + ", versionCode: "
                + versionCode);
        return true;
    }

    @TargetApi(Build.VERSION_CODES.M)
    @SuppressFBWarnings("DLS_DEAD_LOCAL_STORE")  // we replace old value before using it
    private static void allowActionModeDestroyOnNonUiThread(ActionMode actionMode) {
        // LG Email app dismisses ActionMode whenever InputConnection#setComposingText() or
        // InputConnection#commitText() occurs. But they do on ImeThread, not on UI thread and
        // this causes crashes in two places.
        try {
            // Part I: post ActionMode.Callback2#onDestroyActionMode() on UI thread.
            final ActionMode.Callback2 c = (Callback2) getField(actionMode, "mCallback");
            setField(actionMode, "mCallback", new ActionMode.Callback2() {
                @Override
                public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                    return c.onCreateActionMode(mode, menu);
                }

                @Override
                public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
                    return c.onPrepareActionMode(mode, menu);
                }

                @Override
                public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
                    return c.onActionItemClicked(mode, item);
                }

                @Override
                public void onDestroyActionMode(final ActionMode mode) {
                    ThreadUtils.postOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            c.onDestroyActionMode(mode);
                        }
                    });
                }
            });

            // Part II: post PopupWindow#dismiss() on UI thread.
            final Object floatingToolbar = getField(actionMode, "mFloatingToolbar");
            final Object popup = getField(floatingToolbar, "mPopup");
            final ViewGroup contentContainer = (ViewGroup) getField(popup, "mContentContainer");
            final PopupWindow popupWindow = (PopupWindow) getField(popup, "mPopupWindow");
            Method createExitAnimation = floatingToolbar.getClass().getDeclaredMethod(
                    "createExitAnimation", View.class, int.class, AnimatorListener.class);
            createExitAnimation.setAccessible(true);
            Object newDismissAnimation = createExitAnimation.invoke(
                    null, contentContainer, 150, new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            ThreadUtils.postOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    popupWindow.dismiss();
                                    contentContainer.removeAllViews();
                                }
                            });
                        }
                    });
            setField(popup, "mDismissAnimation", newDismissAnimation);
        } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException
                | NoSuchMethodException | InvocationTargetException e) {
            // Ignore exception and just return.
        } catch (Exception e) {
            Log.w(TAG, "Error occurred during LGEmailActionModeWorkaround: ", e);
        }
    }

    private static Object getField(Object obj, String fieldName)
            throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
        Field f = obj.getClass().getDeclaredField(fieldName);
        f.setAccessible(true);
        return f.get(obj);
    }

    private static void setField(Object obj, String fieldName, Object value)
            throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
        Field f = obj.getClass().getDeclaredField(fieldName);
        f.setAccessible(true);
        f.set(obj, value);
    }
}