/*
 * Copyright (C) 2012 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;

import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.os.RemoteException;
import android.util.Log;
import android.util.Slog;
import android.view.Choreographer;
import android.view.Display;
import android.view.IWindowSession;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewRootImpl;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.view.animation.Transformation;
import android.widget.FrameLayout;

public class UniverseBackground extends FrameLayout {
    static final String TAG = "UniverseBackground";
    static final boolean SPEW = false;
    static final boolean CHATTY = false;

    final IWindowSession mSession;
    final View mContent;
    final View mBottomAnchor;

    final Runnable mAnimationCallback = new Runnable() {
        @Override
        public void run() {
            doAnimation(mChoreographer.getFrameTimeNanos());
        }
    };

    // fling gesture tuning parameters, scaled to display density
    private float mSelfExpandVelocityPx; // classic value: 2000px/s
    private float mSelfCollapseVelocityPx; // classic value: 2000px/s (will be negated to collapse "up")
    private float mFlingExpandMinVelocityPx; // classic value: 200px/s
    private float mFlingCollapseMinVelocityPx; // classic value: 200px/s
    private float mCollapseMinDisplayFraction; // classic value: 0.08 (25px/min(320px,480px) on G1)
    private float mExpandMinDisplayFraction; // classic value: 0.5 (drag open halfway to expand)
    private float mFlingGestureMaxXVelocityPx; // classic value: 150px/s

    private float mExpandAccelPx; // classic value: 2000px/s/s
    private float mCollapseAccelPx; // classic value: 2000px/s/s (will be negated to collapse "up")

    static final int STATE_CLOSED = 0;
    static final int STATE_OPENING = 1;
    static final int STATE_OPEN = 2;
    private int mState = STATE_CLOSED;

    private float mDragStartX, mDragStartY;
    private float mAverageX, mAverageY;

    // position
    private int[] mPositionTmp = new int[2];
    private boolean mExpanded;
    private boolean mExpandedVisible;

    private boolean mTracking;
    private VelocityTracker mVelocityTracker;

    private Choreographer mChoreographer;
    private boolean mAnimating;
    private boolean mClosing; // only valid when mAnimating; indicates the initial acceleration
    private float mAnimY;
    private float mAnimVel;
    private float mAnimAccel;
    private long mAnimLastTimeNanos;
    private boolean mAnimatingReveal = false;

    private int mYDelta = 0;
    private Transformation mUniverseTransform = new Transformation();
    private final float[] mTmpFloats = new float[9];

    public UniverseBackground(Context context) {
        super(context);
        setBackgroundColor(0xff000000);
        mSession = WindowManagerGlobal.getWindowSession(context.getMainLooper());
        mContent = View.inflate(context, R.layout.universe, null);
        addView(mContent);
        mContent.findViewById(R.id.close).setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                animateCollapse();
            }
        });
        mBottomAnchor = mContent.findViewById(R.id.bottom);
        mChoreographer = Choreographer.getInstance();
        loadDimens();
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        loadDimens();
    }

    private void loadDimens() {
        final Resources res = getContext().getResources();
        mSelfExpandVelocityPx = res.getDimension(R.dimen.self_expand_velocity);
        mSelfCollapseVelocityPx = res.getDimension(R.dimen.self_collapse_velocity);
        mFlingExpandMinVelocityPx = res.getDimension(R.dimen.fling_expand_min_velocity);
        mFlingCollapseMinVelocityPx = res.getDimension(R.dimen.fling_collapse_min_velocity);

        mCollapseMinDisplayFraction = res.getFraction(R.dimen.collapse_min_display_fraction, 1, 1);
        mExpandMinDisplayFraction = res.getFraction(R.dimen.expand_min_display_fraction, 1, 1);

        mExpandAccelPx = res.getDimension(R.dimen.expand_accel);
        mCollapseAccelPx = res.getDimension(R.dimen.collapse_accel);

        mFlingGestureMaxXVelocityPx = res.getDimension(R.dimen.fling_gesture_max_x_velocity);
    }

    private void computeAveragePos(MotionEvent event) {
        final int num = event.getPointerCount();
        float x = 0, y = 0;
        for (int i=0; i<num; i++) {
            x += event.getX(i);
            y += event.getY(i);
        }
        mAverageX = x / num;
        mAverageY = y / num;
    }

    private void sendUniverseTransform() {
        if (getWindowToken() != null) {
            mUniverseTransform.getMatrix().getValues(mTmpFloats);
            try {
                mSession.setUniverseTransform(getWindowToken(), mUniverseTransform.getAlpha(),
                        mTmpFloats[Matrix.MTRANS_X], mTmpFloats[Matrix.MTRANS_Y],
                        mTmpFloats[Matrix.MSCALE_X], mTmpFloats[Matrix.MSKEW_Y],
                        mTmpFloats[Matrix.MSKEW_X], mTmpFloats[Matrix.MSCALE_Y]);
            } catch (RemoteException e) {
            }
        }
    }

    public WindowManager.LayoutParams getLayoutParams() {
        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.TYPE_UNIVERSE_BACKGROUND,
                    0
                    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
                PixelFormat.OPAQUE);
        // this will allow the window to run in an overlay on devices that support this
        if (ActivityManager.isHighEndGfx()) {
            lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        }
        lp.setTitle("UniverseBackground");
        lp.windowAnimations = 0;
        return lp;
    }

    private int getExpandedViewMaxHeight() {
        return mBottomAnchor.getTop();
    }

    public void animateCollapse() {
        animateCollapse(1.0f);
    }

    public void animateCollapse(float velocityMultiplier) {
        if (SPEW) {
            Slog.d(TAG, "animateCollapse(): mExpanded=" + mExpanded
                    + " mExpandedVisible=" + mExpandedVisible
                    + " mExpanded=" + mExpanded
                    + " mAnimating=" + mAnimating
                    + " mAnimY=" + mAnimY
                    + " mAnimVel=" + mAnimVel);
        }

        mState = STATE_CLOSED;
        if (!mExpandedVisible) {
            return;
        }

        int y;
        if (mAnimating) {
            y = (int)mAnimY;
        } else {
            y = getExpandedViewMaxHeight()-1;
        }
        // Let the fling think that we're open so it goes in the right direction
        // and doesn't try to re-open the windowshade.
        mExpanded = true;
        prepareTracking(y, false);
        performFling(y, -mSelfCollapseVelocityPx*velocityMultiplier, true);
    }

    private void updateUniverseScale() {
        if (mYDelta > 0) {
            int w = getWidth();
            int h = getHeight();
            float scale = (h-mYDelta+.5f) / (float)h;
            mUniverseTransform.getMatrix().setScale(scale, scale, w/2, h);
            if (CHATTY) Log.i(TAG, "w=" + w + " h=" + h + " scale=" + scale
                    + ": " + mUniverseTransform);
            sendUniverseTransform();
            if (getVisibility() != VISIBLE) {
                setVisibility(VISIBLE);
            }
        } else {
            if (CHATTY) Log.i(TAG, "mYDelta=" + mYDelta);
            mUniverseTransform.clear();
            sendUniverseTransform();
            if (getVisibility() == VISIBLE) {
                setVisibility(GONE);
            }
        }
    }

    void resetLastAnimTime() {
        mAnimLastTimeNanos = System.nanoTime();
        if (SPEW) {
            Throwable t = new Throwable();
            t.fillInStackTrace();
            Slog.d(TAG, "resetting last anim time=" + mAnimLastTimeNanos, t);
        }
    }

    void doAnimation(long frameTimeNanos) {
        if (mAnimating) {
            if (SPEW) Slog.d(TAG, "doAnimation dt=" + (frameTimeNanos - mAnimLastTimeNanos));
            if (SPEW) Slog.d(TAG, "doAnimation before mAnimY=" + mAnimY);
            incrementAnim(frameTimeNanos);
            if (SPEW) {
                Slog.d(TAG, "doAnimation after  mAnimY=" + mAnimY);
            }

            if (mAnimY >= getExpandedViewMaxHeight()-1 && !mClosing) {
                if (SPEW) Slog.d(TAG, "Animation completed to expanded state.");
                mAnimating = false;
                mYDelta = getExpandedViewMaxHeight();
                updateUniverseScale();
                mExpanded = true;
                mState = STATE_OPEN;
                return;
            }

            if (mAnimY <= 0 && mClosing) {
                if (SPEW) Slog.d(TAG, "Animation completed to collapsed state.");
                mAnimating = false;
                mYDelta = 0;
                updateUniverseScale();
                mExpanded = false;
                mState = STATE_CLOSED;
                return;
            }

            mYDelta = (int)mAnimY;
            updateUniverseScale();
            mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION,
                    mAnimationCallback, null);
        }
    }

    void stopTracking() {
        mTracking = false;
        mVelocityTracker.recycle();
        mVelocityTracker = null;
    }

    void incrementAnim(long frameTimeNanos) {
        final long deltaNanos = Math.max(frameTimeNanos - mAnimLastTimeNanos, 0);
        final float t = deltaNanos * 0.000000001f;                  // ns -> s
        final float y = mAnimY;
        final float v = mAnimVel;                                   // px/s
        final float a = mAnimAccel;                                 // px/s/s
        mAnimY = y + (v*t) + (0.5f*a*t*t);                          // px
        mAnimVel = v + (a*t);                                       // px/s
        mAnimLastTimeNanos = frameTimeNanos;                        // ns
        //Slog.d(TAG, "y=" + y + " v=" + v + " a=" + a + " t=" + t + " mAnimY=" + mAnimY
        //        + " mAnimAccel=" + mAnimAccel);
    }

    void prepareTracking(int y, boolean opening) {
        if (CHATTY) {
            Slog.d(TAG, "panel: beginning to track the user's touch, y=" + y + " opening=" + opening);
        }

        mTracking = true;
        mVelocityTracker = VelocityTracker.obtain();
        if (opening) {
            mAnimAccel = mExpandAccelPx;
            mAnimVel = mFlingExpandMinVelocityPx;
            mAnimY = y;
            mAnimating = true;
            mAnimatingReveal = true;
            resetLastAnimTime();
            mExpandedVisible = true;
        }
        if (mAnimating) {
            mAnimating = false;
            mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION,
                    mAnimationCallback, null);
        }
    }

    void performFling(int y, float vel, boolean always) {
        if (CHATTY) {
            Slog.d(TAG, "panel: will fling, y=" + y + " vel=" + vel);
        }

        mAnimatingReveal = false;

        mAnimY = y;
        mAnimVel = vel;

        //Slog.d(TAG, "starting with mAnimY=" + mAnimY + " mAnimVel=" + mAnimVel);

        if (mExpanded) {
            if (!always && (
                    vel > mFlingCollapseMinVelocityPx
                    || (y > (getExpandedViewMaxHeight()*(1f-mCollapseMinDisplayFraction)) &&
                        vel > -mFlingExpandMinVelocityPx))) {
                // We are expanded, but they didn't move sufficiently to cause
                // us to retract.  Animate back to the expanded position.
                mAnimAccel = mExpandAccelPx;
                if (vel < 0) {
                    mAnimVel = 0;
                }
            }
            else {
                // We are expanded and are now going to animate away.
                mAnimAccel = -mCollapseAccelPx;
                if (vel > 0) {
                    mAnimVel = 0;
                }
            }
        } else {
            if (always || (
                    vel > mFlingExpandMinVelocityPx
                    || (y > (getExpandedViewMaxHeight()*(1f-mExpandMinDisplayFraction)) &&
                        vel > -mFlingCollapseMinVelocityPx))) {
                // We are collapsed, and they moved enough to allow us to
                // expand.  Animate in the notifications.
                mAnimAccel = mExpandAccelPx;
                if (vel < 0) {
                    mAnimVel = 0;
                }
            }
            else {
                // We are collapsed, but they didn't move sufficiently to cause
                // us to retract.  Animate back to the collapsed position.
                mAnimAccel = -mCollapseAccelPx;
                if (vel > 0) {
                    mAnimVel = 0;
                }
            }
        }
        //Slog.d(TAG, "mAnimY=" + mAnimY + " mAnimVel=" + mAnimVel
        //        + " mAnimAccel=" + mAnimAccel);

        resetLastAnimTime();
        mAnimating = true;
        mClosing = mAnimAccel < 0;
        mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION,
                mAnimationCallback, null);
        mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION,
                mAnimationCallback, null);

        stopTracking();
    }

    private void trackMovement(MotionEvent event) {
        mVelocityTracker.addMovement(event);
    }

    public boolean consumeEvent(MotionEvent event) {
        if (mState == STATE_CLOSED) {
            if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
                // Second finger down, time to start opening!
                computeAveragePos(event);
                mDragStartX = mAverageX;
                mDragStartY = mAverageY;
                mYDelta = 0;
                mUniverseTransform.clear();
                sendUniverseTransform();
                setVisibility(VISIBLE);
                mState = STATE_OPENING;
                prepareTracking((int)mDragStartY, true);
                mVelocityTracker.clear();
                trackMovement(event);
                return true;
            }
            return false;
        }

        if (mState == STATE_OPENING) {
            if (event.getActionMasked() == MotionEvent.ACTION_UP
                    || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
                mVelocityTracker.computeCurrentVelocity(1000);
                computeAveragePos(event);

                float yVel = mVelocityTracker.getYVelocity();
                boolean negative = yVel < 0;

                float xVel = mVelocityTracker.getXVelocity();
                if (xVel < 0) {
                    xVel = -xVel;
                }
                if (xVel > mFlingGestureMaxXVelocityPx) {
                    xVel = mFlingGestureMaxXVelocityPx; // limit how much we care about the x axis
                }

                float vel = (float)Math.hypot(yVel, xVel);
                if (negative) {
                    vel = -vel;
                }

                if (CHATTY) {
                    Slog.d(TAG, String.format("gesture: vraw=(%f,%f) vnorm=(%f,%f) vlinear=%f",
                        mVelocityTracker.getXVelocity(),
                        mVelocityTracker.getYVelocity(),
                        xVel, yVel,
                        vel));
                }

                performFling((int)mAverageY, vel, false);
                mState = STATE_OPEN;
                return true;
            }

            computeAveragePos(event);
            mYDelta = (int)(mAverageY - mDragStartY);
            if (mYDelta > getExpandedViewMaxHeight()) {
                mYDelta = getExpandedViewMaxHeight();
            }
            updateUniverseScale();
            return true;
        }

        return false;
    }
}
