package com.android.launcher3.ui;

import android.app.SearchManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Point;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.Direction;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
import android.test.InstrumentationTestCase;
import android.view.MotionEvent;

import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherClings;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.ManagedProfileHeuristic;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Base class for all instrumentation tests providing various utility methods.
 */
public class LauncherInstrumentationTestCase extends InstrumentationTestCase {

    public static final long DEFAULT_UI_TIMEOUT = 3000;

    protected UiDevice mDevice;
    protected Context mTargetContext;
    protected String mTargetPackage;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        mDevice = UiDevice.getInstance(getInstrumentation());
        mTargetContext = getInstrumentation().getTargetContext();
        mTargetPackage = mTargetContext.getPackageName();
    }

    protected void lockRotation(boolean naturalOrientation) throws RemoteException {
        Utilities.getPrefs(mTargetContext)
                .edit()
                .putBoolean(Utilities.ALLOW_ROTATION_PREFERENCE_KEY, !naturalOrientation)
                .commit();

        if (naturalOrientation) {
            mDevice.setOrientationNatural();
        } else {
            mDevice.setOrientationRight();
        }
    }

    /**
     * Starts the launcher activity in the target package and returns the Launcher instance.
     */
    protected Launcher startLauncher() {
        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
                .addCategory(Intent.CATEGORY_HOME)
                .setPackage(mTargetPackage)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        return (Launcher) getInstrumentation().startActivitySync(homeIntent);
    }

    /**
     * Grants the launcher permission to bind widgets.
     */
    protected void grantWidgetPermission() throws IOException {
        // Check bind widget permission
        if (mTargetContext.getPackageManager().checkPermission(
                mTargetPackage, android.Manifest.permission.BIND_APPWIDGET)
                != PackageManager.PERMISSION_GRANTED) {
            ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation().executeShellCommand(
                    "appwidget grantbind --package " + mTargetPackage);
            // Read the input stream fully.
            FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
            while (fis.read() != -1);
            fis.close();
        }
    }

    /**
     * Opens all apps and returns the recycler view
     */
    protected UiObject2 openAllApps() {
        if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
            // clicking on the page indicator brings up all apps tray on non tablets.
            findViewById(R.id.page_indicator).click();
        } else {
            mDevice.wait(Until.findObject(
                    By.desc(mTargetContext.getString(R.string.all_apps_button_label))),
                    DEFAULT_UI_TIMEOUT).click();
        }
        return findViewById(R.id.apps_list_view);
    }

    /**
     * Opens widget tray and returns the recycler view.
     */
    protected UiObject2 openWidgetsTray() {
        mDevice.pressMenu(); // Enter overview mode.
        mDevice.wait(Until.findObject(
                By.text(mTargetContext.getString(R.string.widget_button_text)
                        .toUpperCase(Locale.getDefault()))), DEFAULT_UI_TIMEOUT).click();
        return findViewById(R.id.widgets_list_view);
    }

    /**
     * Scrolls the {@param container} until it finds an object matching {@param condition}.
     * @return the matching object.
     */
    protected UiObject2 scrollAndFind(UiObject2 container, BySelector condition) {
        do {
            UiObject2 widget = container.findObject(condition);
            if (widget != null) {
                return widget;
            }
        } while (container.scroll(Direction.DOWN, 1f));
        return container.findObject(condition);
    }

    /**
     * Drags an icon to the center of homescreen.
     */
    protected void dragToWorkspace(UiObject2 icon) {
        Point center = icon.getVisibleCenter();

        // Action Down
        sendPointer(MotionEvent.ACTION_DOWN, center);

        // Wait until "Remove/Delete target is visible
        assertNotNull(findViewById(R.id.delete_target_text));

        Point moveLocation = findViewById(R.id.drag_layer).getVisibleCenter();

        // Move to center
        while(!moveLocation.equals(center)) {
            center.x = getNextMoveValue(moveLocation.x, center.x);
            center.y = getNextMoveValue(moveLocation.y, center.y);
            sendPointer(MotionEvent.ACTION_MOVE, center);
        }
        sendPointer(MotionEvent.ACTION_UP, center);

        // Wait until remove target is gone.
        mDevice.wait(Until.gone(getSelectorForId(R.id.delete_target_text)), DEFAULT_UI_TIMEOUT);
    }

    private int getNextMoveValue(int targetValue, int oldValue) {
        if (targetValue - oldValue > 10) {
            return oldValue + 10;
        } else if (targetValue - oldValue < -10) {
            return oldValue - 10;
        } else {
            return targetValue;
        }
    }

    private void sendPointer(int action, Point point) {
        MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
                SystemClock.uptimeMillis(), action, point.x, point.y, 0);
        getInstrumentation().sendPointerSync(event);
        event.recycle();
    }

    /**
     * Removes all icons from homescreen and hotseat.
     */
    public void clearHomescreen() throws Throwable {
        LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
        LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
                LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
        LauncherClings.markFirstRunClingDismissed(mTargetContext);
        ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mTargetContext);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Reset the loader state
                LauncherAppState.getInstance().getModel().resetLoadedState(true, true);
            }
        });
    }

    /**
     * Runs the callback on the UI thread and returns the result.
     */
    protected <T> T getOnUiThread(final Callable<T> callback) {
        final AtomicReference<T> result = new AtomicReference<>(null);
        try {
            runTestOnUiThread(new Runnable() {
                @Override
                public void run() {
                    try {
                        result.set(callback.call());
                    } catch (Exception e) { }
                }
            });
        } catch (Throwable t) { }
        return result.get();
    }

    /**
     * Finds a widget provider which can fit on the home screen.
     * @param hasConfigureScreen if true, a provider with a config screen is returned.
     */
    protected LauncherAppWidgetProviderInfo findWidgetProvider(final boolean hasConfigureScreen) {
        LauncherAppWidgetProviderInfo info = getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
            @Override
            public LauncherAppWidgetProviderInfo call() throws Exception {
                InvariantDeviceProfile idv =
                        LauncherAppState.getInstance().getInvariantDeviceProfile();

                ComponentName searchComponent = ((SearchManager) mTargetContext
                        .getSystemService(Context.SEARCH_SERVICE)).getGlobalSearchActivity();
                String searchPackage = searchComponent == null
                        ? null : searchComponent.getPackageName();

                for (AppWidgetProviderInfo info :
                        AppWidgetManagerCompat.getInstance(mTargetContext).getAllProviders()) {
                    if ((info.configure != null) ^ hasConfigureScreen) {
                        continue;
                    }
                    // Exclude the widgets in search package, as Launcher already binds them in
                    // QSB, so they can cause conflicts.
                    if (info.provider.getPackageName().equals(searchPackage)) {
                        continue;
                    }
                    LauncherAppWidgetProviderInfo widgetInfo = LauncherAppWidgetProviderInfo
                            .fromProviderInfo(mTargetContext, info);
                    if (widgetInfo.minSpanX >= idv.numColumns
                            || widgetInfo.minSpanY >= idv.numRows) {
                        continue;
                    }
                    return widgetInfo;
                }
                return null;
            }
        });
        if (info == null) {
            throw new IllegalArgumentException("No valid widget provider");
        }
        return info;
    }

    protected UiObject2 findViewById(int id) {
        return mDevice.wait(Until.findObject(getSelectorForId(id)), DEFAULT_UI_TIMEOUT);
    }

    protected BySelector getSelectorForId(int id) {
        String name = mTargetContext.getResources().getResourceEntryName(id);
        return By.res(mTargetPackage, name);
    }
}