/*
 * Copyright (C) 2019 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 com.android.quickstep;

import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_DOWN;
import static android.view.MotionEvent.ACTION_UP;

import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.RectF;
import android.util.Log;
import android.view.MotionEvent;
import android.view.Surface;

import com.android.launcher3.R;
import com.android.launcher3.ResourceUtils;
import com.android.launcher3.util.DisplayController.Info;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * Maintains state for supporting nav bars and tracking their gestures in multiple orientations.
 * See {@link OrientationRectF#applyTransformToRotation(MotionEvent, int, boolean)} for
 * transformation of MotionEvents from one orientation's coordinate space to another's.
 *
 * This class only supports single touch/pointer gesture tracking for touches started in a supported
 * nav bar region.
 */
class OrientationTouchTransformer {

    private static class CurrentDisplay {
        public Point size;
        public int rotation;

        CurrentDisplay() {
            this.size = new Point(0, 0);
            this.rotation = 0;
        }

        CurrentDisplay(Point size, int rotation) {
            this.size = size;
            this.rotation = rotation;
        }

        @Override
        public String toString() {
            return "CurrentDisplay:"
                    + " rotation: " + rotation
                    + " size: " + size;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            CurrentDisplay display = (CurrentDisplay) o;
            if (rotation != display.rotation) return false;

            return Objects.equals(size, display.size);
        }

        @Override
        public int hashCode() {
            return Objects.hash(size, rotation);
        }
    };

    private static final String TAG = "OrientationTouchTransformer";
    private static final boolean DEBUG = false;

    private static final int QUICKSTEP_ROTATION_UNINITIALIZED = -1;

    private final Map<CurrentDisplay, OrientationRectF> mSwipeTouchRegions =
            new HashMap<CurrentDisplay, OrientationRectF>();
    private final RectF mAssistantLeftRegion = new RectF();
    private final RectF mAssistantRightRegion = new RectF();
    private final RectF mOneHandedModeRegion = new RectF();
    private CurrentDisplay mCurrentDisplay = new CurrentDisplay();
    private int mNavBarGesturalHeight;
    private final int mNavBarLargerGesturalHeight;
    private boolean mEnableMultipleRegions;
    private Resources mResources;
    private OrientationRectF mLastRectTouched;
    /**
     * The rotation of the last touched nav bar, whether that be through the last region the user
     * touched down on or valid rotation user turned their device to.
     * Note this is different than
     * {@link #mQuickStepStartingRotation} as it always updates its value on every touch whereas
     * mQuickstepStartingRotation only updates when device rotation matches touch rotation.
     */
    private int mActiveTouchRotation;
    private SysUINavigationMode.Mode mMode;
    private QuickStepContractInfo mContractInfo;

    /**
     * Represents if we're currently in a swipe "session" of sorts. If value is
     * QUICKSTEP_ROTATION_UNINITIALIZED, then user has not tapped on an active nav region.
     * Otherwise it will be the rotation of the display when the user first interacted with the
     * active nav bar region.
     * The "session" ends when {@link #enableMultipleRegions(boolean, Info)} is
     * called - usually from a timeout or if user starts interacting w/ the foreground app.
     *
     * This is different than {@link #mLastRectTouched} as it can get reset by the system whereas
     * the rect is purely used for tracking touch interactions and usually this "session" will
     * outlast the touch interaction.
     */
    private int mQuickStepStartingRotation = QUICKSTEP_ROTATION_UNINITIALIZED;

    /** For testability */
    interface QuickStepContractInfo {
        float getWindowCornerRadius();
    }


    OrientationTouchTransformer(Resources resources, SysUINavigationMode.Mode mode,
            QuickStepContractInfo contractInfo) {
        mResources = resources;
        mMode = mode;
        mContractInfo = contractInfo;
        mNavBarGesturalHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
        mNavBarLargerGesturalHeight = ResourceUtils.getDimenByName(
                ResourceUtils.NAVBAR_BOTTOM_GESTURE_LARGER_SIZE, resources,
                mNavBarGesturalHeight);
    }

    private void refreshTouchRegion(Info info, Resources newRes) {
        // Swipe touch regions are independent of nav mode, so we have to clear them explicitly
        // here to avoid, for ex, a nav region for 2-button rotation 0 being used for 3-button mode
        // It tries to cache and reuse swipe regions whenever possible based only on rotation
        mResources = newRes;
        mSwipeTouchRegions.clear();
        resetSwipeRegions(info);
    }

    void setNavigationMode(SysUINavigationMode.Mode newMode, Info info, Resources newRes) {
        if (DEBUG) {
            Log.d(TAG, "setNavigationMode new: " + newMode + " oldMode: " + mMode + " " + this);
        }
        if (mMode == newMode) {
            return;
        }
        this.mMode = newMode;
        refreshTouchRegion(info, newRes);
    }

    void setGesturalHeight(int newGesturalHeight, Info info, Resources newRes) {
        if (mNavBarGesturalHeight == newGesturalHeight) {
            return;
        }
        mNavBarGesturalHeight = newGesturalHeight;
        refreshTouchRegion(info, newRes);
    }

    /**
     * Sets the current nav bar region to listen to events for as determined by
     * {@param info}. If multiple nav bar regions are enabled, then this region will be added
     * alongside other regions.
     * Ok to call multiple times
     *
     * @see #enableMultipleRegions(boolean, Info)
     */
    void createOrAddTouchRegion(Info info) {
        mCurrentDisplay = new CurrentDisplay(info.currentSize, info.rotation);

        if (mQuickStepStartingRotation > QUICKSTEP_ROTATION_UNINITIALIZED
                && mCurrentDisplay.rotation == mQuickStepStartingRotation) {
            // User already was swiping and the current screen is same rotation as the starting one
            // Remove active nav bars in other rotations except for the one we started out in
            resetSwipeRegions(info);
            return;
        }
        OrientationRectF region = mSwipeTouchRegions.get(mCurrentDisplay);
        if (region != null) {
            return;
        }

        if (mEnableMultipleRegions) {
            mSwipeTouchRegions.put(mCurrentDisplay, createRegionForDisplay(info));
        } else {
            resetSwipeRegions(info);
        }
    }

    /**
     * Call when we want to start tracking nav bar touch regions in multiple orientations.
     * ALSO, you BETTER call this with {@param enableMultipleRegions} set to false once you're done.
     *
     * @param enableMultipleRegions Set to true to start tracking multiple nav bar regions
     * @param info The current displayInfo which will be the start of the quickswitch gesture
     */
    void enableMultipleRegions(boolean enableMultipleRegions, Info info) {
        mEnableMultipleRegions = enableMultipleRegions &&
                mMode != SysUINavigationMode.Mode.TWO_BUTTONS;
        if (mEnableMultipleRegions) {
            mQuickStepStartingRotation = info.rotation;
        } else {
            mActiveTouchRotation = 0;
            mQuickStepStartingRotation = QUICKSTEP_ROTATION_UNINITIALIZED;
        }
        resetSwipeRegions(info);
    }

    /**
     * Call when removing multiple regions to swipe from, but still in active quickswitch mode (task
     * list is still frozen).
     * Ex. This would be called when user has quickswitched to the same app rotation that
     * they started quickswitching in, indicating that extra nav regions can be ignored. Calling
     * this will update the value of {@link #mActiveTouchRotation}
     *
     * @param displayInfo The display whos rotation will be used as the current active rotation
     */
    void setSingleActiveRegion(Info displayInfo) {
        mActiveTouchRotation = displayInfo.rotation;
        resetSwipeRegions(displayInfo);
    }

    /**
     * Only saves the swipe region represented by {@param region}, clears the
     * rest from {@link #mSwipeTouchRegions}
     * To be called whenever we want to stop tracking more than one swipe region.
     * Ok to call multiple times.
     */
    private void resetSwipeRegions(Info region) {
        if (DEBUG) {
            Log.d(TAG, "clearing all regions except rotation: " + mCurrentDisplay.rotation);
        }

        mCurrentDisplay = new CurrentDisplay(region.currentSize, region.rotation);
        OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplay);
        if (regionToKeep == null) {
            regionToKeep = createRegionForDisplay(region);
        }
        mSwipeTouchRegions.clear();
        mSwipeTouchRegions.put(mCurrentDisplay, regionToKeep);
        updateAssistantRegions(regionToKeep);
    }

    private void resetSwipeRegions() {
        OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplay);
        mSwipeTouchRegions.clear();
        if (regionToKeep != null) {
            mSwipeTouchRegions.put(mCurrentDisplay, regionToKeep);
            updateAssistantRegions(regionToKeep);
        }
    }

    private OrientationRectF createRegionForDisplay(Info display) {
        if (DEBUG) {
            Log.d(TAG, "creating rotation region for: " + mCurrentDisplay.rotation
            + " with mode: " + mMode + " displayRotation: " + display.rotation);
        }

        Point size = display.currentSize;
        int rotation = display.rotation;
        int touchHeight = mNavBarGesturalHeight;
        OrientationRectF orientationRectF =
                new OrientationRectF(0, 0, size.x, size.y, rotation);
        if (mMode == SysUINavigationMode.Mode.NO_BUTTON) {
            orientationRectF.top = orientationRectF.bottom - touchHeight;
            updateAssistantRegions(orientationRectF);
        } else {
            mAssistantLeftRegion.setEmpty();
            mAssistantRightRegion.setEmpty();
            int navbarSize = getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
            switch (rotation) {
                case Surface.ROTATION_90:
                    orientationRectF.left = orientationRectF.right
                            - navbarSize;
                    break;
                case Surface.ROTATION_270:
                    orientationRectF.right = orientationRectF.left
                            + navbarSize;
                    break;
                default:
                    orientationRectF.top = orientationRectF.bottom - touchHeight;
            }
        }
        // One handed gestural only active on portrait mode
        mOneHandedModeRegion.set(0, orientationRectF.bottom - mNavBarLargerGesturalHeight,
                size.x, size.y);

        return orientationRectF;
    }

    private void updateAssistantRegions(OrientationRectF orientationRectF) {
        int navbarHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
        int assistantWidth = mResources.getDimensionPixelSize(R.dimen.gestures_assistant_width);
        float assistantHeight = Math.max(navbarHeight, mContractInfo.getWindowCornerRadius());
        mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = orientationRectF.bottom;
        mAssistantLeftRegion.top = mAssistantRightRegion.top =
                orientationRectF.bottom - assistantHeight;

        mAssistantLeftRegion.left = 0;
        mAssistantLeftRegion.right = assistantWidth;

        mAssistantRightRegion.right = orientationRectF.right;
        mAssistantRightRegion.left = orientationRectF.right - assistantWidth;
    }

    boolean touchInAssistantRegion(MotionEvent ev) {
        return mAssistantLeftRegion.contains(ev.getX(), ev.getY())
                || mAssistantRightRegion.contains(ev.getX(), ev.getY());

    }

    boolean touchInOneHandedModeRegion(MotionEvent ev) {
        return mOneHandedModeRegion.contains(ev.getX(), ev.getY());
    }

    private int getNavbarSize(String resName) {
        return ResourceUtils.getNavbarSize(resName, mResources);
    }

    boolean touchInValidSwipeRegions(float x, float y) {
        if (DEBUG) {
            Log.d(TAG, "touchInValidSwipeRegions " + x + "," + y + " in "
                    + mLastRectTouched + " this: " + this);
        }
        if (mLastRectTouched != null) {
            return mLastRectTouched.contains(x, y);
        }
        return false;
    }

    int getCurrentActiveRotation() {
        return mActiveTouchRotation;
    }

    int getQuickStepStartingRotation() {
        return mQuickStepStartingRotation;
    }

    public void transform(MotionEvent event) {
        int eventAction = event.getActionMasked();
        switch (eventAction) {
            case ACTION_MOVE: {
                if (mLastRectTouched == null) {
                    return;
                }
                mLastRectTouched.applyTransformFromRotation(event, mCurrentDisplay.rotation, true);
                break;
            }
            case ACTION_CANCEL:
            case ACTION_UP: {
                if (mLastRectTouched == null) {
                    return;
                }
                mLastRectTouched.applyTransformFromRotation(event, mCurrentDisplay.rotation, true);
                mLastRectTouched = null;
                break;
            }
            case ACTION_POINTER_DOWN:
            case ACTION_DOWN: {
                if (mLastRectTouched != null) {
                    return;
                }

                for (OrientationRectF rect : mSwipeTouchRegions.values()) {
                    if (rect == null) {
                        continue;
                    }
                    if (rect.applyTransformFromRotation(event, mCurrentDisplay.rotation, false)) {
                        mLastRectTouched = rect;
                        mActiveTouchRotation = rect.getRotation();
                        if (mEnableMultipleRegions
                                && mCurrentDisplay.rotation == mActiveTouchRotation) {
                            // TODO(b/154580671) might make this block unnecessary
                            // Start a touch session for the default nav region for the display
                            mQuickStepStartingRotation = mLastRectTouched.getRotation();
                            resetSwipeRegions();
                        }
                        if (DEBUG) {
                            Log.d(TAG, "set active region: " + rect);
                        }
                        return;
                    }
                }
                break;
            }
        }
    }

    public void dump(PrintWriter pw) {
        pw.println("OrientationTouchTransformerState: ");
        pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
        pw.println("  lastTouchedRegion=" + mLastRectTouched);
        pw.println("  multipleRegionsEnabled=" + mEnableMultipleRegions);
        StringBuilder regions = new StringBuilder("  currentTouchableRotations=");
        for (CurrentDisplay key: mSwipeTouchRegions.keySet()) {
            OrientationRectF rectF = mSwipeTouchRegions.get(key);
            regions.append(rectF).append(" ");
        }
        pw.println(regions.toString());
        pw.println("  mNavBarGesturalHeight=" + mNavBarGesturalHeight);
        pw.println("  mNavBarLargerGesturalHeight=" + mNavBarLargerGesturalHeight);
        pw.println("  mOneHandedModeRegion=" + mOneHandedModeRegion);
    }
}
