/*
 * Copyright (C) 2015 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 android.graphics.drawable;

import android.animation.Animator;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.view.DisplayListCanvas;
import android.view.RenderNodeAnimator;

import java.util.ArrayList;

/**
 * Abstract class that handles hardware/software hand-off and lifecycle for
 * animated ripple foreground and background components.
 */
abstract class RippleComponent {
    private final RippleDrawable mOwner;

    /** Bounds used for computing max radius. May be modified by the owner. */
    protected final Rect mBounds;

    /** Whether we can use hardware acceleration for the exit animation. */
    private boolean mHasDisplayListCanvas;

    private boolean mHasPendingHardwareAnimator;
    private RenderNodeAnimatorSet mHardwareAnimator;

    private Animator mSoftwareAnimator;

    /** Whether we have an explicit maximum radius. */
    private boolean mHasMaxRadius;

    /** How big this ripple should be when fully entered. */
    protected float mTargetRadius;

    /** Screen density used to adjust pixel-based constants. */
    protected float mDensityScale;

    /**
     * If set, force all ripple animations to not run on RenderThread, even if it would be
     * available.
     */
    private final boolean mForceSoftware;

    public RippleComponent(RippleDrawable owner, Rect bounds, boolean forceSoftware) {
        mOwner = owner;
        mBounds = bounds;
        mForceSoftware = forceSoftware;
    }

    public void onBoundsChange() {
        if (!mHasMaxRadius) {
            mTargetRadius = getTargetRadius(mBounds);
            onTargetRadiusChanged(mTargetRadius);
        }
    }

    public final void setup(float maxRadius, int densityDpi) {
        if (maxRadius >= 0) {
            mHasMaxRadius = true;
            mTargetRadius = maxRadius;
        } else {
            mTargetRadius = getTargetRadius(mBounds);
        }

        mDensityScale = densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;

        onTargetRadiusChanged(mTargetRadius);
    }

    private static float getTargetRadius(Rect bounds) {
        final float halfWidth = bounds.width() / 2.0f;
        final float halfHeight = bounds.height() / 2.0f;
        return (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
    }

    /**
     * Starts a ripple enter animation.
     *
     * @param fast whether the ripple should enter quickly
     */
    public final void enter(boolean fast) {
        cancel();

        mSoftwareAnimator = createSoftwareEnter(fast);

        if (mSoftwareAnimator != null) {
            mSoftwareAnimator.start();
        }
    }

    /**
     * Starts a ripple exit animation.
     */
    public final void exit() {
        cancel();

        if (mHasDisplayListCanvas) {
            // We don't have access to a canvas here, but we expect one on the
            // next frame. We'll start the render thread animation then.
            mHasPendingHardwareAnimator = true;

            // Request another frame.
            invalidateSelf();
        } else {
            mSoftwareAnimator = createSoftwareExit();
            mSoftwareAnimator.start();
        }
    }

    /**
     * Cancels all animations. Software animation values are left in the
     * current state, while hardware animation values jump to the end state.
     */
    public void cancel() {
        cancelSoftwareAnimations();
        endHardwareAnimations();
    }

    /**
     * Ends all animations, jumping values to the end state.
     */
    public void end() {
        endSoftwareAnimations();
        endHardwareAnimations();
    }

    /**
     * Draws the ripple to the canvas, inheriting the paint's color and alpha
     * properties.
     *
     * @param c the canvas to which the ripple should be drawn
     * @param p the paint used to draw the ripple
     * @return {@code true} if something was drawn, {@code false} otherwise
     */
    public boolean draw(Canvas c, Paint p) {
        final boolean hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated()
                && c instanceof DisplayListCanvas;
        if (mHasDisplayListCanvas != hasDisplayListCanvas) {
            mHasDisplayListCanvas = hasDisplayListCanvas;

            if (!hasDisplayListCanvas) {
                // We've switched from hardware to non-hardware mode. Panic.
                endHardwareAnimations();
            }
        }

        if (hasDisplayListCanvas) {
            final DisplayListCanvas hw = (DisplayListCanvas) c;
            startPendingAnimation(hw, p);

            if (mHardwareAnimator != null) {
                return drawHardware(hw);
            }
        }

        return drawSoftware(c, p);
    }

    /**
     * Populates {@code bounds} with the maximum drawing bounds of the ripple
     * relative to its center. The resulting bounds should be translated into
     * parent drawable coordinates before use.
     *
     * @param bounds the rect to populate with drawing bounds
     */
    public void getBounds(Rect bounds) {
        final int r = (int) Math.ceil(mTargetRadius);
        bounds.set(-r, -r, r, r);
    }

    /**
     * Starts the pending hardware animation, if available.
     *
     * @param hw hardware canvas on which the animation should draw
     * @param p paint whose properties the hardware canvas should use
     */
    private void startPendingAnimation(DisplayListCanvas hw, Paint p) {
        if (mHasPendingHardwareAnimator) {
            mHasPendingHardwareAnimator = false;

            mHardwareAnimator = createHardwareExit(new Paint(p));
            mHardwareAnimator.start(hw);

            // Preemptively jump the software values to the end state now that
            // the hardware exit has read whatever values it needs.
            jumpValuesToExit();
        }
    }

    /**
     * Cancels any current software animations, leaving the values in their
     * current state.
     */
    private void cancelSoftwareAnimations() {
        if (mSoftwareAnimator != null) {
            mSoftwareAnimator.cancel();
            mSoftwareAnimator = null;
        }
    }

    /**
     * Ends any current software animations, jumping the values to their end
     * state.
     */
    private void endSoftwareAnimations() {
        if (mSoftwareAnimator != null) {
            mSoftwareAnimator.end();
            mSoftwareAnimator = null;
        }
    }

    /**
     * Ends any pending or current hardware animations.
     * <p>
     * Hardware animations can't synchronize values back to the software
     * thread, so there is no "cancel" equivalent.
     */
    private void endHardwareAnimations() {
        if (mHardwareAnimator != null) {
            mHardwareAnimator.end();
            mHardwareAnimator = null;
        }

        if (mHasPendingHardwareAnimator) {
            mHasPendingHardwareAnimator = false;

            // Manually jump values to their exited state. Normally we'd do that
            // later when starting the hardware exit, but we're aborting early.
            jumpValuesToExit();
        }
    }

    protected final void invalidateSelf() {
        mOwner.invalidateSelf(false);
    }

    protected final boolean isHardwareAnimating() {
        return mHardwareAnimator != null && mHardwareAnimator.isRunning()
                || mHasPendingHardwareAnimator;
    }

    protected final void onHotspotBoundsChanged() {
        if (!mHasMaxRadius) {
            final float halfWidth = mBounds.width() / 2.0f;
            final float halfHeight = mBounds.height() / 2.0f;
            final float targetRadius = (float) Math.sqrt(halfWidth * halfWidth
                    + halfHeight * halfHeight);

            onTargetRadiusChanged(targetRadius);
        }
    }

    /**
     * Called when the target radius changes.
     *
     * @param targetRadius the new target radius
     */
    protected void onTargetRadiusChanged(float targetRadius) {
        // Stub.
    }

    protected abstract Animator createSoftwareEnter(boolean fast);

    protected abstract Animator createSoftwareExit();

    protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p);

    protected abstract boolean drawHardware(DisplayListCanvas c);

    protected abstract boolean drawSoftware(Canvas c, Paint p);

    /**
     * Called when the hardware exit is cancelled. Jumps software values to end
     * state to ensure that software and hardware values are synchronized.
     */
    protected abstract void jumpValuesToExit();

    public static class RenderNodeAnimatorSet {
        private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>();

        public void add(RenderNodeAnimator anim) {
            mAnimators.add(anim);
        }

        public void clear() {
            mAnimators.clear();
        }

        public void start(DisplayListCanvas target) {
            if (target == null) {
                throw new IllegalArgumentException("Hardware canvas must be non-null");
            }

            final ArrayList<RenderNodeAnimator> animators = mAnimators;
            final int N = animators.size();
            for (int i = 0; i < N; i++) {
                final RenderNodeAnimator anim = animators.get(i);
                anim.setTarget(target);
                anim.start();
            }
        }

        public void cancel() {
            final ArrayList<RenderNodeAnimator> animators = mAnimators;
            final int N = animators.size();
            for (int i = 0; i < N; i++) {
                final RenderNodeAnimator anim = animators.get(i);
                anim.cancel();
            }
        }

        public void end() {
            final ArrayList<RenderNodeAnimator> animators = mAnimators;
            final int N = animators.size();
            for (int i = 0; i < N; i++) {
                final RenderNodeAnimator anim = animators.get(i);
                anim.end();
            }
        }

        public boolean isRunning() {
            final ArrayList<RenderNodeAnimator> animators = mAnimators;
            final int N = animators.size();
            for (int i = 0; i < N; i++) {
                final RenderNodeAnimator anim = animators.get(i);
                if (anim.isRunning()) {
                    return true;
                }
            }
            return false;
        }
    }
}
