/*
 * Copyright (C) 2014 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.systemui.assist;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.animation.Interpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;

import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;

public class AssistOrbView extends FrameLayout {

    private final int mCircleMinSize;
    private final int mBaseMargin;
    private final int mStaticOffset;
    private final Paint mBackgroundPaint = new Paint();
    private final Rect mCircleRect = new Rect();
    private final Rect mStaticRect = new Rect();
    private final Interpolator mOvershootInterpolator = new OvershootInterpolator();

    private boolean mClipToOutline;
    private final int mMaxElevation;
    private float mOutlineAlpha;
    private float mOffset;
    private float mCircleSize;
    private ImageView mLogo;
    private float mCircleAnimationEndValue;

    private ValueAnimator mOffsetAnimator;
    private ValueAnimator mCircleAnimator;

    private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener
            = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            applyCircleSize((float) animation.getAnimatedValue());
            updateElevation();
        }
    };
    private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            mCircleAnimator = null;
        }
    };
    private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener
            = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mOffset = (float) animation.getAnimatedValue();
            updateLayout();
        }
    };


    public AssistOrbView(Context context) {
        this(context, null);
    }

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

    public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        setOutlineProvider(new ViewOutlineProvider() {
            @Override
            public void getOutline(View view, Outline outline) {
                if (mCircleSize > 0.0f) {
                    outline.setOval(mCircleRect);
                } else {
                    outline.setEmpty();
                }
                outline.setAlpha(mOutlineAlpha);
            }
        });
        setWillNotDraw(false);
        mCircleMinSize = context.getResources().getDimensionPixelSize(
                R.dimen.assist_orb_size);
        mBaseMargin = context.getResources().getDimensionPixelSize(
                R.dimen.assist_orb_base_margin);
        mStaticOffset = context.getResources().getDimensionPixelSize(
                R.dimen.assist_orb_travel_distance);
        mMaxElevation = context.getResources().getDimensionPixelSize(
                R.dimen.assist_orb_elevation);
        mBackgroundPaint.setAntiAlias(true);
        mBackgroundPaint.setColor(getResources().getColor(R.color.assist_orb_color));
    }

    public ImageView getLogo() {
        return mLogo;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBackground(canvas);
    }

    private void drawBackground(Canvas canvas) {
        canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2,
                mBackgroundPaint);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mLogo = findViewById(R.id.search_logo);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight());
        if (changed) {
            updateCircleRect(mStaticRect, mStaticOffset, true);
        }
    }

    public void animateCircleSize(float circleSize, long duration,
            long startDelay, Interpolator interpolator) {
        if (circleSize == mCircleAnimationEndValue) {
            return;
        }
        if (mCircleAnimator != null) {
            mCircleAnimator.cancel();
        }
        mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize);
        mCircleAnimator.addUpdateListener(mCircleUpdateListener);
        mCircleAnimator.addListener(mClearAnimatorListener);
        mCircleAnimator.setInterpolator(interpolator);
        mCircleAnimator.setDuration(duration);
        mCircleAnimator.setStartDelay(startDelay);
        mCircleAnimator.start();
        mCircleAnimationEndValue = circleSize;
    }

    private void applyCircleSize(float circleSize) {
        mCircleSize = circleSize;
        updateLayout();
    }

    private void updateElevation() {
        float t = (mStaticOffset - mOffset) / (float) mStaticOffset;
        t = 1.0f - Math.max(t, 0.0f);
        float offset = t * mMaxElevation;
        setElevation(offset);
    }

    /**
     * Animates the offset to the edge of the screen.
     *
     * @param offset The offset to apply.
     * @param startDelay The desired start delay if animated.
     *
     * @param interpolator The desired interpolator if animated. If null,
     *                     a default interpolator will be taken designed for appearing or
     *                     disappearing.
     */
    private void animateOffset(float offset, long duration, long startDelay,
            Interpolator interpolator) {
        if (mOffsetAnimator != null) {
            mOffsetAnimator.removeAllListeners();
            mOffsetAnimator.cancel();
        }
        mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset);
        mOffsetAnimator.addUpdateListener(mOffsetUpdateListener);
        mOffsetAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mOffsetAnimator = null;
            }
        });
        mOffsetAnimator.setInterpolator(interpolator);
        mOffsetAnimator.setStartDelay(startDelay);
        mOffsetAnimator.setDuration(duration);
        mOffsetAnimator.start();
    }

    private void updateLayout() {
        updateCircleRect();
        updateLogo();
        invalidateOutline();
        invalidate();
        updateClipping();
    }

    private void updateClipping() {
        boolean clip = mCircleSize < mCircleMinSize;
        if (clip != mClipToOutline) {
            setClipToOutline(clip);
            mClipToOutline = clip;
        }
    }

    private void updateLogo() {
        float translationX = (mCircleRect.left + mCircleRect.right) / 2.0f - mLogo.getWidth() / 2.0f;
        float translationY = (mCircleRect.top + mCircleRect.bottom) / 2.0f
                - mLogo.getHeight() / 2.0f - mCircleMinSize / 7f;
        float t = (mStaticOffset - mOffset) / (float) mStaticOffset;
        translationY += t * mStaticOffset * 0.1f;
        float alpha = 1.0f-t;
        alpha = Math.max((alpha - 0.5f) * 2.0f, 0);
        mLogo.setImageAlpha((int) (alpha * 255));
        mLogo.setTranslationX(translationX);
        mLogo.setTranslationY(translationY);
    }

    private void updateCircleRect() {
        updateCircleRect(mCircleRect, mOffset, false);
    }

    private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) {
        int left, top;
        float circleSize = useStaticSize ? mCircleMinSize : mCircleSize;
        left = (int) (getWidth() - circleSize) / 2;
        top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset);
        rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize));
    }

    public void startExitAnimation(long delay) {
        animateCircleSize(0, 200, delay, Interpolators.FAST_OUT_LINEAR_IN);
        animateOffset(0, 200, delay, Interpolators.FAST_OUT_LINEAR_IN);
    }

    public void startEnterAnimation() {
        applyCircleSize(0);
        post(new Runnable() {
            @Override
            public void run() {
                animateCircleSize(mCircleMinSize, 300, 0 /* delay */, mOvershootInterpolator);
                animateOffset(mStaticOffset, 400, 0 /* delay */, Interpolators.LINEAR_OUT_SLOW_IN);
            }
        });
    }

    public void reset() {
        mClipToOutline = false;
        mBackgroundPaint.setAlpha(255);
        mOutlineAlpha = 1.0f;
    }

    @Override
    public boolean hasOverlappingRendering() {
        // not really true but it's ok during an animation, as it's never permanent
        return false;
    }
}
