/*
 * Copyright (C) 2021 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.launcher3.keyboard;

import static android.app.Activity.DEFAULT_KEYS_SEARCH_LOCAL;

import static com.android.launcher3.LauncherState.SPRING_LOADED;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.widget.TextView;

import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;

import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.PagedView;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.statemanager.StateManager.StateListener;
import com.android.launcher3.touch.ItemLongClickListener;
import com.android.launcher3.util.Themes;

import java.util.ArrayList;
import java.util.Objects;
import java.util.function.ToIntBiFunction;
import java.util.function.ToIntFunction;

/**
 * A floating view to allow keyboard navigation across virtual nodes
 */
public class KeyboardDragAndDropView extends AbstractFloatingView
        implements Insettable, StateListener<LauncherState> {

    private static final long MINOR_AXIS_WEIGHT = 13;

    private final ArrayList<Integer> mIntList = new ArrayList<>();
    private final ArrayList<DragAndDropAccessibilityDelegate> mDelegates = new ArrayList<>();
    private final ArrayList<VirtualNodeInfo> mNodes = new ArrayList<>();

    private final Rect mTempRect = new Rect();
    private final Rect mTempRect2 = new Rect();
    private final AccessibilityNodeInfoCompat mTempNodeInfo = AccessibilityNodeInfoCompat.obtain();

    private final RectFocusIndicator mFocusIndicator;

    private final Launcher mLauncher;
    private VirtualNodeInfo mCurrentSelection;


    public KeyboardDragAndDropView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public KeyboardDragAndDropView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mLauncher = Launcher.getLauncher(context);
        mFocusIndicator = new RectFocusIndicator(this);
        setWillNotDraw(false);
    }

    @Override
    protected void handleClose(boolean animate) {
        mLauncher.getDragLayer().removeView(this);
        mLauncher.getStateManager().removeStateListener(this);
        mLauncher.setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
        mIsOpen = false;
    }

    @Override
    protected boolean isOfType(int type) {
        return (type & TYPE_DRAG_DROP_POPUP) != 0;
    }

    @Override
    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
        // Consume all touch
        return true;
    }

    @Override
    public void setInsets(Rect insets) {
        setPadding(insets.left, insets.top, insets.right, insets.bottom);
    }

    @Override
    public void onStateTransitionStart(LauncherState toState) {
        if (toState != SPRING_LOADED) {
            close(false);
        }
    }

    @Override
    public void onStateTransitionComplete(LauncherState finalState) {
        if (mCurrentSelection != null) {
            setCurrentSelection(mCurrentSelection);
        }
    }

    private void setCurrentSelection(VirtualNodeInfo nodeInfo) {
        mCurrentSelection = nodeInfo;
        ((TextView) findViewById(R.id.label))
                .setText(nodeInfo.populate(mTempNodeInfo).getContentDescription());

        Rect bounds = new Rect();
        mTempNodeInfo.getBoundsInParent(bounds);
        View host = nodeInfo.delegate.getHost();
        ViewParent parent = host.getParent();
        if (parent instanceof PagedView) {
            PagedView pv = (PagedView) parent;
            int pageIndex = pv.indexOfChild(host);

            pv.setCurrentPage(pageIndex);
            bounds.offset(pv.getScrollX() - pv.getScrollForPage(pageIndex), 0);
        }
        float[] pos = new float[] {bounds.left, bounds.top, bounds.right, bounds.bottom};
        Utilities.getDescendantCoordRelativeToAncestor(host, mLauncher.getDragLayer(), pos, true);

        new RectF(pos[0], pos[1], pos[2], pos[3]).roundOut(bounds);
        mFocusIndicator.changeFocus(bounds, true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mFocusIndicator.draw(canvas);
    }

    @Override
    public boolean dispatchUnhandledMove(View focused, int direction) {
        VirtualNodeInfo nodeInfo = getNextSelection(direction);
        if (nodeInfo == null) {
            return false;
        }
        setCurrentSelection(nodeInfo);
        return true;
    }

    /**
     * Focus finding logic:
     * Collect all virtual nodes in reading order (used for forward and backwards).
     * Then find the closest view by comparing the distances spatially. Since it is a move
     * operation. consider all cell sizes to be approximately of the same size.
     */
    private VirtualNodeInfo getNextSelection(int direction) {
        // Collect all virtual nodes
        mDelegates.clear();
        mNodes.clear();

        Folder openFolder = Folder.getOpen(mLauncher);
        PagedView pv = openFolder == null ? mLauncher.getWorkspace() : openFolder.getContent();
        int count = pv.getPageCount();
        for (int i = 0; i < count; i++) {
            mDelegates.add(((CellLayout) pv.getChildAt(i)).getDragAndDropAccessibilityDelegate());
        }
        if (openFolder == null) {
            mDelegates.add(pv.getNextPage() + 1,
                    mLauncher.getHotseat().getDragAndDropAccessibilityDelegate());
        }
        mDelegates.forEach(delegate -> {
            mIntList.clear();
            delegate.getVisibleVirtualViews(mIntList);
            mIntList.forEach(id -> mNodes.add(new VirtualNodeInfo(delegate, id)));
        });

        if (mNodes.isEmpty()) {
            return null;
        }
        int index = mNodes.indexOf(mCurrentSelection);
        if (mCurrentSelection == null || index < 0) {
            return null;
        }
        int totalNodes = mNodes.size();

        final ToIntBiFunction<Rect, Rect> majorAxis;
        final ToIntFunction<Rect> minorAxis;

        switch (direction) {
            case View.FOCUS_RIGHT:
                majorAxis = (source, dest) -> dest.left - source.left;
                minorAxis = Rect::centerY;
                break;
            case View.FOCUS_LEFT:
                majorAxis = (source, dest) -> source.left - dest.left;
                minorAxis = Rect::centerY;
                break;
            case View.FOCUS_UP:
                majorAxis = (source, dest) -> source.top - dest.top;
                minorAxis = Rect::centerX;
                break;
            case View.FOCUS_DOWN:
                majorAxis = (source, dest) -> dest.top - source.top;
                minorAxis = Rect::centerX;
                break;
            case View.FOCUS_FORWARD:
                return mNodes.get((index + 1) % totalNodes);
            case View.FOCUS_BACKWARD:
                return mNodes.get((index + totalNodes - 1) % totalNodes);
            default:
                // Unknown direction
                return null;
        }
        mCurrentSelection.populate(mTempNodeInfo).getBoundsInScreen(mTempRect);

        float minWeight = Float.MAX_VALUE;
        VirtualNodeInfo match = null;
        for (int i = 0; i < totalNodes; i++) {
            VirtualNodeInfo node = mNodes.get(i);
            node.populate(mTempNodeInfo).getBoundsInScreen(mTempRect2);

            int majorAxisWeight = majorAxis.applyAsInt(mTempRect, mTempRect2);
            if (majorAxisWeight <= 0) {
                continue;
            }
            int minorAxisWeight = minorAxis.applyAsInt(mTempRect2)
                    - minorAxis.applyAsInt(mTempRect);

            float weight = majorAxisWeight * majorAxisWeight
                    + minorAxisWeight * minorAxisWeight * MINOR_AXIS_WEIGHT;
            if (weight < minWeight) {
                minWeight = weight;
                match = node;
            }
        }
        return match;
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_ENTER && mCurrentSelection != null) {
            mCurrentSelection.delegate.onPerformActionForVirtualView(
                    mCurrentSelection.id, AccessibilityNodeInfoCompat.ACTION_CLICK, null);
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }

    /**
     * Shows the keyboard drag popup for the provided view
     */
    public void showForIcon(View icon, ItemInfo item, DragOptions dragOptions) {
        mIsOpen = true;
        mLauncher.getDragLayer().addView(this);
        mLauncher.getStateManager().addStateListener(this);

        // Find current selection
        CellLayout currentParent = (CellLayout) icon.getParent().getParent();
        float[] iconPos = new float[] {currentParent.getCellWidth() / 2,
                currentParent.getCellHeight() / 2};
        Utilities.getDescendantCoordRelativeToAncestor(icon, currentParent, iconPos, false);

        ItemLongClickListener.beginDrag(icon, mLauncher, item, dragOptions);

        DragAndDropAccessibilityDelegate dndDelegate =
                currentParent.getDragAndDropAccessibilityDelegate();
        setCurrentSelection(new VirtualNodeInfo(
                dndDelegate, dndDelegate.getVirtualViewAt(iconPos[0], iconPos[1])));

        mLauncher.setDefaultKeyMode(Activity.DEFAULT_KEYS_DISABLE);
        requestFocus();
    }

    private static class VirtualNodeInfo {
        public final DragAndDropAccessibilityDelegate delegate;
        public final int id;

        VirtualNodeInfo(DragAndDropAccessibilityDelegate delegate, int id) {
            this.id = id;
            this.delegate = delegate;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof VirtualNodeInfo)) {
                return false;
            }
            VirtualNodeInfo that = (VirtualNodeInfo) o;
            return id == that.id && delegate.equals(that.delegate);
        }

        public AccessibilityNodeInfoCompat populate(AccessibilityNodeInfoCompat nodeInfo) {
            delegate.onPopulateNodeForVirtualView(id, nodeInfo);
            return nodeInfo;
        }

        public void getBounds(AccessibilityNodeInfoCompat nodeInfo, Rect out) {
            delegate.onPopulateNodeForVirtualView(id, nodeInfo);
            nodeInfo.getBoundsInScreen(out);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, delegate);
        }
    }

    private static class RectFocusIndicator extends ItemFocusIndicatorHelper<Rect> {

        RectFocusIndicator(View container) {
            super(container, Themes.getColorAccent(container.getContext()));
            mPaint.setStrokeWidth(container.getResources()
                    .getDimension(R.dimen.keyboard_drag_stroke_width));
            mPaint.setStyle(Style.STROKE);
        }

        @Override
        public void viewToRect(Rect item, Rect outRect) {
            outRect.set(item);
        }
    }
}
