/*
 * Copyright (C) 2008 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.dragndrop;

import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;

import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
import static com.android.launcher3.Utilities.getBadge;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Path;
import android.graphics.Picture;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PictureDrawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;

import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;

import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;

/** A custom view for rendering an icon, folder, shortcut or widget during drag-n-drop. */
public abstract class DragView<T extends Context & ActivityContext> extends FrameLayout {

    public static final int VIEW_ZOOM_DURATION = 150;

    private final View mContent;
    // The following are only used for rendering mContent directly during drag-n-drop.
    @Nullable private ViewGroup.LayoutParams mContentViewLayoutParams;
    @Nullable private ViewGroup mContentViewParent;
    private int mContentViewInParentViewIndex = -1;
    private final int mWidth;
    private final int mHeight;

    private final int mBlurSizeOutline;
    protected final int mRegistrationX;
    protected final int mRegistrationY;
    private final float mInitialScale;
    protected final float mScaleOnDrop;
    protected final int[] mTempLoc = new int[2];

    private final RunnableList mOnDragStartCallback = new RunnableList();

    private Point mDragVisualizeOffset = null;
    private Rect mDragRegion = null;
    protected final T mActivity;
    private final BaseDragLayer<T> mDragLayer;
    private boolean mHasDrawn = false;

    final ValueAnimator mAnim;
    // Whether mAnim has started. Unlike mAnim.isStarted(), this is true even after mAnim ends.
    private boolean mAnimStarted;

    private int mLastTouchX;
    private int mLastTouchY;
    private int mAnimatedShiftX;
    private int mAnimatedShiftY;

    // Below variable only needed IF FeatureFlags.LAUNCHER3_SPRING_ICONS is {@code true}
    private Drawable mBgSpringDrawable, mFgSpringDrawable;
    private SpringFloatValue mTranslateX, mTranslateY;
    private Path mScaledMaskPath;
    private Drawable mBadge;

    public DragView(T launcher, Drawable drawable, int registrationX,
            int registrationY, final float initialScale, final float scaleOnDrop,
            final float finalScaleDps) {
        this(launcher, getViewFromDrawable(launcher, drawable),
                drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
                registrationX, registrationY, initialScale, scaleOnDrop, finalScaleDps);
    }

    /**
     * Construct the drag view.
     * <p>
     * The registration point is the point inside our view that the touch events should
     * be centered upon.
     * @param activity The Launcher instance/ActivityContext this DragView is in.
     * @param content the view content that is attached to the drag view.
     * @param width the width of the dragView
     * @param height the height of the dragView
     * @param initialScale The view that we're dragging around.  We scale it up when we draw it.
     * @param registrationX The x coordinate of the registration point.
     * @param registrationY The y coordinate of the registration point.
     * @param scaleOnDrop the scale used in the drop animation.
     * @param finalScaleDps the scale used in the zoom out animation when the drag view is shown.
     */
    public DragView(T activity, View content, int width, int height, int registrationX,
            int registrationY, final float initialScale, final float scaleOnDrop,
            final float finalScaleDps) {
        super(activity);
        mActivity = activity;
        mDragLayer = activity.getDragLayer();

        mContent = content;
        mWidth = width;
        mHeight = height;
        mContentViewLayoutParams = mContent.getLayoutParams();
        if (mContent.getParent() instanceof ViewGroup) {
            mContentViewParent = (ViewGroup) mContent.getParent();
            mContentViewInParentViewIndex = mContentViewParent.indexOfChild(mContent);
            mContentViewParent.removeView(mContent);
        }

        addView(content, new LayoutParams(width, height));

        // If there is already a scale set on the content, we don't want to clip the children.
        if (content.getScaleX() != 1 || content.getScaleY() != 1) {
            setClipChildren(false);
            setClipToPadding(false);
        }

        final float scale = (width + finalScaleDps) / width;

        // Set the initial scale to avoid any jumps
        setScaleX(initialScale);
        setScaleY(initialScale);

        // Animate the view into the correct position
        mAnim = ValueAnimator.ofFloat(0f, 1f);
        mAnim.setDuration(VIEW_ZOOM_DURATION);
        mAnim.addUpdateListener(animation -> {
            final float value = (Float) animation.getAnimatedValue();
            setScaleX(initialScale + (value * (scale - initialScale)));
            setScaleY(initialScale + (value * (scale - initialScale)));
            if (!isAttachedToWindow()) {
                animation.cancel();
            }
        });
        mAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                mAnimStarted = true;
            }
        });

        setDragRegion(new Rect(0, 0, width, height));

        // The point in our scaled bitmap that the touch events are located
        mRegistrationX = registrationX;
        mRegistrationY = registrationY;

        mInitialScale = initialScale;
        mScaleOnDrop = scaleOnDrop;

        // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass
        measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));

        mBlurSizeOutline = getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
        setElevation(getResources().getDimension(R.dimen.drag_elevation));
        setWillNotDraw(false);
    }

    /**
     * Initialize {@code #mIconDrawable} if the item can be represented using
     * an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}.
     */
    @TargetApi(Build.VERSION_CODES.O)
    public void setItemInfo(final ItemInfo info) {
        if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
                && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION
                && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
                && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
            return;
        }
        // Load the adaptive icon on a background thread and add the view in ui thread.
        MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
            Object[] outObj = new Object[1];
            int w = mWidth;
            int h = mHeight;
            Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h, outObj);

            if (dr instanceof AdaptiveIconDrawable) {
                int blurMargin = (int) mActivity.getResources()
                        .getDimension(R.dimen.blur_size_medium_outline) / 2;

                Rect bounds = new Rect(0, 0, w, h);
                bounds.inset(blurMargin, blurMargin);
                // Badge is applied after icon normalization so the bounds for badge should not
                // be scaled down due to icon normalization.
                Rect badgeBounds = new Rect(bounds);
                mBadge = getBadge(mActivity, info, outObj[0]);
                mBadge.setBounds(badgeBounds);

                // Do not draw the background in case of folder as its translucent
                final boolean shouldDrawBackground = !(dr instanceof FolderAdaptiveIcon);

                try (LauncherIcons li = LauncherIcons.obtain(mActivity)) {
                    Drawable nDr; // drawable to be normalized
                    if (shouldDrawBackground) {
                        nDr = dr;
                    } else {
                        // Since we just want the scale, avoid heavy drawing operations
                        nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
                    }
                    Utilities.scaleRectAboutCenter(bounds,
                            li.getNormalizer().getScale(nDr, null, null, null));
                }
                AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr;

                // Shrink very tiny bit so that the clip path is smaller than the original bitmap
                // that has anti aliased edges and shadows.
                Rect shrunkBounds = new Rect(bounds);
                Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
                adaptiveIcon.setBounds(shrunkBounds);
                final Path mask = adaptiveIcon.getIconMask();

                mTranslateX = new SpringFloatValue(DragView.this,
                        w * AdaptiveIconDrawable.getExtraInsetFraction());
                mTranslateY = new SpringFloatValue(DragView.this,
                        h * AdaptiveIconDrawable.getExtraInsetFraction());

                bounds.inset(
                        (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
                        (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction())
                );
                mBgSpringDrawable = adaptiveIcon.getBackground();
                if (mBgSpringDrawable == null) {
                    mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
                }
                mBgSpringDrawable.setBounds(bounds);
                mFgSpringDrawable = adaptiveIcon.getForeground();
                if (mFgSpringDrawable == null) {
                    mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
                }
                mFgSpringDrawable.setBounds(bounds);

                new Handler(Looper.getMainLooper()).post(() -> mOnDragStartCallback.add(() -> {
                    // TODO: Consider fade-in animation
                    // Assign the variable on the UI thread to avoid race conditions.
                    mScaledMaskPath = mask;
                    // Avoid relayout as we do not care about children affecting layout
                    removeAllViewsInLayout();

                    if (info.isDisabled()) {
                        FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null);
                        d.setIsDisabled(true);
                        mBgSpringDrawable.setColorFilter(d.getColorFilter());
                        mFgSpringDrawable.setColorFilter(d.getColorFilter());
                        mBadge.setColorFilter(d.getColorFilter());
                    }
                    invalidate();
                }));
            }
        });
    }

    /**
     * Called when pre-drag finishes for an icon
     */
    public void onDragStart() {
        mOnDragStartCallback.executeAllAndDestroy();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
    }

    public int getDragRegionWidth() {
        return mDragRegion.width();
    }

    public int getDragRegionHeight() {
        return mDragRegion.height();
    }

    public void setDragVisualizeOffset(Point p) {
        mDragVisualizeOffset = p;
    }

    public Point getDragVisualizeOffset() {
        return mDragVisualizeOffset;
    }

    public void setDragRegion(Rect r) {
        mDragRegion = r;
    }

    public Rect getDragRegion() {
        return mDragRegion;
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        // Draw after the content
        mHasDrawn = true;
        if (mScaledMaskPath != null) {
            int cnt = canvas.save();
            canvas.clipPath(mScaledMaskPath);
            mBgSpringDrawable.draw(canvas);
            canvas.translate(mTranslateX.mValue, mTranslateY.mValue);
            mFgSpringDrawable.draw(canvas);
            canvas.restoreToCount(cnt);
            mBadge.draw(canvas);
        }
    }

    public void crossFadeContent(Drawable crossFadeDrawable, int duration) {
        if (mContent.getParent() == null) {
            // If the content is already removed, ignore
            return;
        }
        View newContent = getViewFromDrawable(getContext(), crossFadeDrawable);
        newContent.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
        newContent.layout(0, 0, mWidth, mHeight);
        addViewInLayout(newContent, 0, new LayoutParams(mWidth, mHeight));

        AnimatorSet anim = new AnimatorSet();
        anim.play(ObjectAnimator.ofFloat(newContent, VIEW_ALPHA, 0, 1));
        anim.play(ObjectAnimator.ofFloat(mContent, VIEW_ALPHA, 0));
        anim.setDuration(duration).setInterpolator(Interpolators.DEACCEL_1_5);
        anim.start();
    }

    public boolean hasDrawn() {
        return mHasDrawn;
    }

    /**
     * Create a window containing this view and show it.
     *
     * @param touchX the x coordinate the user touched in DragLayer coordinates
     * @param touchY the y coordinate the user touched in DragLayer coordinates
     */
    public void show(int touchX, int touchY) {
        mDragLayer.addView(this);

        // Start the pick-up animation
        BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(mWidth, mHeight);
        lp.customPosition = true;
        setLayoutParams(lp);

        if (mContent != null) {
            // At the drag start, the source view visibility is set to invisible.
            mContent.setVisibility(VISIBLE);
        }

        move(touchX, touchY);
        // Post the animation to skip other expensive work happening on the first frame
        post(mAnim::start);
    }

    public void cancelAnimation() {
        if (mAnim != null && mAnim.isRunning()) {
            mAnim.cancel();
        }
    }

    public boolean isAnimationFinished() {
        return mAnimStarted && !mAnim.isRunning();
    }

    /**
     * Move the window containing this view.
     *
     * @param touchX the x coordinate the user touched in DragLayer coordinates
     * @param touchY the y coordinate the user touched in DragLayer coordinates
     */
    public void move(int touchX, int touchY) {
        if (touchX > 0 && touchY > 0 && mLastTouchX > 0 && mLastTouchY > 0
                && mScaledMaskPath != null) {
            mTranslateX.animateToPos(mLastTouchX - touchX);
            mTranslateY.animateToPos(mLastTouchY - touchY);
        }
        mLastTouchX = touchX;
        mLastTouchY = touchY;
        applyTranslation();
    }

    /**
     * Animate this DragView to the given DragLayer coordinates and then remove it.
     */
    public abstract void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable,
            int duration);

    public void animateShift(final int shiftX, final int shiftY) {
        if (mAnim.isStarted()) {
            return;
        }
        mAnimatedShiftX = shiftX;
        mAnimatedShiftY = shiftY;
        applyTranslation();
        mAnim.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = 1 - animation.getAnimatedFraction();
                mAnimatedShiftX = (int) (fraction * shiftX);
                mAnimatedShiftY = (int) (fraction * shiftY);
                applyTranslation();
            }
        });
    }

    private void applyTranslation() {
        setTranslationX(mLastTouchX - mRegistrationX + mAnimatedShiftX);
        setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY);
    }

    /**
     * Detaches {@link #mContent}, if previously attached, from this view.
     *
     * <p>In the case of no change in the drop position, sets {@code reattachToPreviousParent} to
     * {@code true} to attach the {@link #mContent} back to its previous parent.
     */
    public void detachContentView(boolean reattachToPreviousParent) {
        if (mContent != null && mContentViewParent != null && mContentViewInParentViewIndex >= 0) {
            Picture picture = new Picture();
            mContent.draw(picture.beginRecording(mWidth, mHeight));
            picture.endRecording();
            View view = new View(mActivity);
            view.setBackground(new PictureDrawable(picture));
            view.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
            view.layout(mContent.getLeft(), mContent.getTop(),
                    mContent.getRight(), mContent.getBottom());
            setClipToOutline(mContent.getClipToOutline());
            setOutlineProvider(mContent.getOutlineProvider());
            addViewInLayout(view, indexOfChild(mContent), mContent.getLayoutParams(), true);

            removeViewInLayout(mContent);
            mContent.setVisibility(INVISIBLE);
            mContent.setLayoutParams(mContentViewLayoutParams);
            if (reattachToPreviousParent) {
                mContentViewParent.addView(mContent, mContentViewInParentViewIndex);
            }
            mContentViewParent = null;
            mContentViewInParentViewIndex = -1;
        }
    }

    /**
     * Removes this view from the {@link DragLayer}.
     *
     * <p>If the drag content is a {@link #mContent}, this call doesn't reattach the
     * {@link #mContent} back to its previous parent. To reattach to previous parent, the caller
     * should call {@link #detachContentView} with {@code reattachToPreviousParent} sets to true
     * before this call.
     */
    public void remove() {
        if (getParent() != null) {
            mDragLayer.removeView(DragView.this);
        }
    }

    public int getBlurSizeOutline() {
        return mBlurSizeOutline;
    }

    public float getInitialScale() {
        return mInitialScale;
    }

    @Override
    public boolean hasOverlappingRendering() {
        return false;
    }

    /** Returns the current content view that is rendered in the drag view. */
    public View getContentView() {
        return mContent;
    }

    /**
     * Returns the previous {@link ViewGroup} parent of the {@link #mContent} before the drag
     * content is attached to this view.
     */
    @Nullable
    public ViewGroup getContentViewParent() {
        return mContentViewParent;
    }

    private static class SpringFloatValue {

        private static final FloatPropertyCompat<SpringFloatValue> VALUE =
                new FloatPropertyCompat<SpringFloatValue>("value") {
                    @Override
                    public float getValue(SpringFloatValue object) {
                        return object.mValue;
                    }

                    @Override
                    public void setValue(SpringFloatValue object, float value) {
                        object.mValue = value;
                        object.mView.invalidate();
                    }
                };

        // Following three values are fine tuned with motion ux designer
        private static final int STIFFNESS = 4000;
        private static final float DAMPENING_RATIO = 1f;
        private static final int PARALLAX_MAX_IN_DP = 8;

        private final View mView;
        private final SpringAnimation mSpring;
        private final float mDelta;

        private float mValue;

        public SpringFloatValue(View view, float range) {
            mView = view;
            mSpring = new SpringAnimation(this, VALUE, 0)
                    .setMinValue(-range).setMaxValue(range)
                    .setSpring(new SpringForce(0)
                            .setDampingRatio(DAMPENING_RATIO)
                            .setStiffness(STIFFNESS));
            mDelta = view.getResources().getDisplayMetrics().density * PARALLAX_MAX_IN_DP;
        }

        public void animateToPos(float value) {
            mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta));
        }
    }

    private static View getViewFromDrawable(Context context, Drawable drawable) {
        ImageView iv = new ImageView(context);
        iv.setImageDrawable(drawable);
        return iv;
    }
}
