/*
 * 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 com.android.packageinstaller.permission.ui;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewRootImpl;
import android.view.WindowManager.LayoutParams;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;

import com.android.internal.widget.ButtonBarLayout;
import com.android.packageinstaller.R;

import java.util.ArrayList;

final class GrantPermissionsDefaultViewHandler
        implements GrantPermissionsViewHandler, OnClickListener {

    public static final String ARG_GROUP_NAME = "ARG_GROUP_NAME";
    public static final String ARG_GROUP_COUNT = "ARG_GROUP_COUNT";
    public static final String ARG_GROUP_INDEX = "ARG_GROUP_INDEX";
    public static final String ARG_GROUP_ICON = "ARG_GROUP_ICON";
    public static final String ARG_GROUP_MESSAGE = "ARG_GROUP_MESSAGE";
    public static final String ARG_GROUP_SHOW_DO_NOT_ASK = "ARG_GROUP_SHOW_DO_NOT_ASK";
    public static final String ARG_GROUP_DO_NOT_ASK_CHECKED = "ARG_GROUP_DO_NOT_ASK_CHECKED";

    // Animation parameters.
    private static final long SIZE_START_DELAY = 300;
    private static final long SIZE_START_LENGTH = 233;
    private static final long FADE_OUT_START_DELAY = 300;
    private static final long FADE_OUT_START_LENGTH = 217;
    private static final long TRANSLATE_START_DELAY = 367;
    private static final long TRANSLATE_LENGTH = 317;
    private static final long GROUP_UPDATE_DELAY = 400;
    private static final long DO_NOT_ASK_CHECK_DELAY = 450;

    private final Context mContext;

    private ResultListener mResultListener;

    private String mGroupName;
    private int mGroupCount;
    private int mGroupIndex;
    private Icon mGroupIcon;
    private CharSequence mGroupMessage;
    private boolean mShowDonNotAsk;
    private boolean mDoNotAskChecked;

    private ImageView mIconView;
    private TextView mCurrentGroupView;
    private TextView mMessageView;
    private CheckBox mDoNotAskCheckbox;
    private Button mAllowButton;

    private ArrayList<ViewHeightController> mHeightControllers;
    private ManualLayoutFrame mRootView;

    // Needed for animation
    private ViewGroup mDescContainer;
    private ViewGroup mCurrentDesc;
    private ViewGroup mNextDesc;

    private ViewGroup mDialogContainer;

    private final Runnable mUpdateGroup = new Runnable() {
        @Override
        public void run() {
            updateGroup();
        }
    };

    GrantPermissionsDefaultViewHandler(Context context) {
        mContext = context;
    }

    @Override
    public GrantPermissionsDefaultViewHandler setResultListener(ResultListener listener) {
        mResultListener = listener;
        return this;
    }

    @Override
    public void saveInstanceState(Bundle arguments) {
        arguments.putString(ARG_GROUP_NAME, mGroupName);
        arguments.putInt(ARG_GROUP_COUNT, mGroupCount);
        arguments.putInt(ARG_GROUP_INDEX, mGroupIndex);
        arguments.putParcelable(ARG_GROUP_ICON, mGroupIcon);
        arguments.putCharSequence(ARG_GROUP_MESSAGE, mGroupMessage);
        arguments.putBoolean(ARG_GROUP_SHOW_DO_NOT_ASK, mShowDonNotAsk);
        arguments.putBoolean(ARG_GROUP_DO_NOT_ASK_CHECKED, mDoNotAskCheckbox.isChecked());
    }

    @Override
    public void loadInstanceState(Bundle savedInstanceState) {
        mGroupName = savedInstanceState.getString(ARG_GROUP_NAME);
        mGroupMessage = savedInstanceState.getCharSequence(ARG_GROUP_MESSAGE);
        mGroupIcon = savedInstanceState.getParcelable(ARG_GROUP_ICON);
        mGroupCount = savedInstanceState.getInt(ARG_GROUP_COUNT);
        mGroupIndex = savedInstanceState.getInt(ARG_GROUP_INDEX);
        mShowDonNotAsk = savedInstanceState.getBoolean(ARG_GROUP_SHOW_DO_NOT_ASK);
        mDoNotAskChecked = savedInstanceState.getBoolean(ARG_GROUP_DO_NOT_ASK_CHECKED);
    }

    @Override
    public void updateUi(String groupName, int groupCount, int groupIndex, Icon icon,
            CharSequence message, boolean showDonNotAsk) {
        mGroupName = groupName;
        mGroupCount = groupCount;
        mGroupIndex = groupIndex;
        mGroupIcon = icon;
        mGroupMessage = message;
        mShowDonNotAsk = showDonNotAsk;
        mDoNotAskChecked = false;
        // If this is a second (or later) permission and the views exist, then animate.
        if (mIconView != null) {
            if (mGroupIndex > 0) {
                // The first message will be announced as the title of the activity, all others
                // we need to announce ourselves.
                mDescContainer.announceForAccessibility(message);
                animateToPermission();
            } else {
                updateDescription();
                updateGroup();
                updateDoNotAskCheckBox();
            }
        }
    }

    private void animateToPermission() {
        if (mHeightControllers == null) {
            // We need to manually control the height of any views heigher than the root that
            // we inflate.  Find all the views up to the root and create ViewHeightControllers for
            // them.
            mHeightControllers = new ArrayList<>();
            ViewRootImpl viewRoot = mRootView.getViewRootImpl();
            ViewParent v = mRootView.getParent();
            addHeightController(mDialogContainer);
            addHeightController(mRootView);
            while (v != viewRoot) {
                addHeightController((View) v);
                v = v.getParent();
            }
            // On the heighest level view, we want to setTop rather than setBottom to control the
            // height, this way the dialog will grow up rather than down.
            ViewHeightController realRootView =
                    mHeightControllers.get(mHeightControllers.size() - 1);
            realRootView.setControlTop(true);
        }

        // Grab the current height/y positions, then wait for the layout to change,
        // so we can get the end height/y positions.
        final SparseArray<Float> startPositions = getViewPositions();
        final int startHeight = mRootView.getLayoutHeight();
        mRootView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
                    int oldTop, int oldRight, int oldBottom) {
                mRootView.removeOnLayoutChangeListener(this);
                SparseArray<Float> endPositions = getViewPositions();
                int endHeight = mRootView.getLayoutHeight();
                if (startPositions.get(R.id.do_not_ask_checkbox) == 0
                        && endPositions.get(R.id.do_not_ask_checkbox) != 0) {
                    // If the checkbox didn't have a position before but has one now then set
                    // the start position to the end position because it just became visible.
                    startPositions.put(R.id.do_not_ask_checkbox,
                            endPositions.get(R.id.do_not_ask_checkbox));
                }
                animateYPos(startPositions, endPositions, endHeight - startHeight);
            }
        });

        // Fade out old description group and scale out the icon for it.
        Interpolator interpolator = AnimationUtils.loadInterpolator(mContext,
                android.R.interpolator.fast_out_linear_in);
        mIconView.animate()
                .scaleX(0)
                .scaleY(0)
                .setStartDelay(FADE_OUT_START_DELAY)
                .setDuration(FADE_OUT_START_LENGTH)
                .setInterpolator(interpolator)
                .start();
        mCurrentDesc.animate()
                .alpha(0)
                .setStartDelay(FADE_OUT_START_DELAY)
                .setDuration(FADE_OUT_START_LENGTH)
                .setInterpolator(interpolator)
                .setListener(null)
                .start();

        // Update the index of the permission after the animations have started.
        mCurrentGroupView.getHandler().postDelayed(mUpdateGroup, GROUP_UPDATE_DELAY);

        // Add the new description and translate it in.
        mNextDesc = (ViewGroup) LayoutInflater.from(mContext).inflate(
                R.layout.permission_description, mDescContainer, false);

        mMessageView = (TextView) mNextDesc.findViewById(R.id.permission_message);
        mIconView = (ImageView) mNextDesc.findViewById(R.id.permission_icon);
        updateDescription();

        int width = mDescContainer.getRootView().getWidth();
        mDescContainer.addView(mNextDesc);
        mNextDesc.setTranslationX(width);

        final View oldDesc = mCurrentDesc;
        // Remove the old view from the description, so that we can shrink if necessary.
        mDescContainer.removeView(oldDesc);
        oldDesc.setPadding(mDescContainer.getLeft(), mDescContainer.getTop(),
                mRootView.getRight() - mDescContainer.getRight(), 0);
        mRootView.addView(oldDesc);

        mCurrentDesc = mNextDesc;
        mNextDesc.animate()
                .translationX(0)
                .setStartDelay(TRANSLATE_START_DELAY)
                .setDuration(TRANSLATE_LENGTH)
                .setInterpolator(AnimationUtils.loadInterpolator(mContext,
                        android.R.interpolator.linear_out_slow_in))
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        // This is the longest animation, when it finishes, we are done.
                        mRootView.removeView(oldDesc);
                    }
                })
                .start();

        boolean visibleBefore = mDoNotAskCheckbox.getVisibility() == View.VISIBLE;
        updateDoNotAskCheckBox();
        boolean visibleAfter = mDoNotAskCheckbox.getVisibility() == View.VISIBLE;
        if (visibleBefore != visibleAfter) {
            Animation anim = AnimationUtils.loadAnimation(mContext,
                    visibleAfter ? android.R.anim.fade_in : android.R.anim.fade_out);
            anim.setStartOffset(visibleAfter ? DO_NOT_ASK_CHECK_DELAY : 0);
            mDoNotAskCheckbox.startAnimation(anim);
        }
    }

    private void addHeightController(View v) {
        ViewHeightController heightController = new ViewHeightController(v);
        heightController.setHeight(v.getHeight());
        mHeightControllers.add(heightController);
    }

    private SparseArray<Float> getViewPositions() {
        SparseArray<Float> locMap = new SparseArray<>();
        final int N = mDialogContainer.getChildCount();
        for (int i = 0; i < N; i++) {
            View child = mDialogContainer.getChildAt(i);
            if (child.getId() <= 0) {
                // Only track views with ids.
                continue;
            }
            locMap.put(child.getId(), child.getY());
        }
        return locMap;
    }

    private void animateYPos(SparseArray<Float> startPositions, SparseArray<Float> endPositions,
            int heightDiff) {
        final int N = startPositions.size();
        for (int i = 0; i < N; i++) {
            int key = startPositions.keyAt(i);
            float start = startPositions.get(key);
            float end = endPositions.get(key);
            if (start != end) {
                final View child = mDialogContainer.findViewById(key);
                child.setTranslationY(start - end);
                child.animate()
                        .setStartDelay(SIZE_START_DELAY)
                        .setDuration(SIZE_START_LENGTH)
                        .translationY(0)
                        .start();
            }
        }
        for (int i = 0; i < mHeightControllers.size(); i++) {
            mHeightControllers.get(i).animateAddHeight(heightDiff);
        }
    }

    @Override
    public View createView() {
        mRootView = (ManualLayoutFrame) LayoutInflater.from(mContext)
                .inflate(R.layout.grant_permissions, null);
        ((ButtonBarLayout) mRootView.findViewById(R.id.button_group)).setAllowStacking(
                Resources.getSystem().getBoolean(
                        com.android.internal.R.bool.allow_stacked_button_bar));

        mDialogContainer = (ViewGroup) mRootView.findViewById(R.id.dialog_container);
        mMessageView = (TextView) mRootView.findViewById(R.id.permission_message);
        mIconView = (ImageView) mRootView.findViewById(R.id.permission_icon);
        mCurrentGroupView = (TextView) mRootView.findViewById(R.id.current_page_text);
        mDoNotAskCheckbox = (CheckBox) mRootView.findViewById(R.id.do_not_ask_checkbox);
        mAllowButton = (Button) mRootView.findViewById(R.id.permission_allow_button);

        mDescContainer = (ViewGroup) mRootView.findViewById(R.id.desc_container);
        mCurrentDesc = (ViewGroup) mRootView.findViewById(R.id.perm_desc_root);

        mAllowButton.setOnClickListener(this);
        mRootView.findViewById(R.id.permission_deny_button).setOnClickListener(this);
        mDoNotAskCheckbox.setOnClickListener(this);

        if (mGroupName != null) {
            updateDescription();
            updateGroup();
            updateDoNotAskCheckBox();
        }

        return mRootView;
    }

    @Override
    public void updateWindowAttributes(LayoutParams outLayoutParams) {
        // No-op
    }

    private void updateDescription() {
        mIconView.setImageDrawable(mGroupIcon.loadDrawable(mContext));
        mMessageView.setText(mGroupMessage);
    }

    private void updateGroup() {
        if (mGroupCount > 1) {
            mCurrentGroupView.setVisibility(View.VISIBLE);
            mCurrentGroupView.setText(mContext.getString(R.string.current_permission_template,
                    mGroupIndex + 1, mGroupCount));
        } else {
            mCurrentGroupView.setVisibility(View.INVISIBLE);
        }
    }

    private void updateDoNotAskCheckBox() {
        if (mShowDonNotAsk) {
            mDoNotAskCheckbox.setVisibility(View.VISIBLE);
            mDoNotAskCheckbox.setOnClickListener(this);
            mDoNotAskCheckbox.setChecked(mDoNotAskChecked);
        } else {
            mDoNotAskCheckbox.setVisibility(View.GONE);
            mDoNotAskCheckbox.setOnClickListener(null);
        }
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.permission_allow_button:
                if (mResultListener != null) {
                    view.clearAccessibilityFocus();
                    mResultListener.onPermissionGrantResult(mGroupName, true, false);
                }
                break;
            case R.id.permission_deny_button:
                mAllowButton.setEnabled(true);
                if (mResultListener != null) {
                    view.clearAccessibilityFocus();
                    mResultListener.onPermissionGrantResult(mGroupName, false,
                            mDoNotAskCheckbox.isChecked());
                }
                break;
            case R.id.do_not_ask_checkbox:
                mAllowButton.setEnabled(!mDoNotAskCheckbox.isChecked());
                break;
        }
    }

    @Override
    public void onBackPressed() {
        if (mResultListener != null) {
            final boolean doNotAskAgain = mDoNotAskCheckbox.isChecked();
            mResultListener.onPermissionGrantResult(mGroupName, false, doNotAskAgain);
        }
    }

    /**
     * Manually controls the height of a view through getBottom/setTop.  Also listens
     * for layout changes and sets the height again to be sure it doesn't change.
     */
    private static final class ViewHeightController implements OnLayoutChangeListener {
        private final View mView;
        private int mHeight;
        private int mNextHeight;
        private boolean mControlTop;
        private ObjectAnimator mAnimator;

        public ViewHeightController(View view) {
            mView = view;
            mView.addOnLayoutChangeListener(this);
        }

        public void setControlTop(boolean controlTop) {
            mControlTop = controlTop;
        }

        public void animateAddHeight(int heightDiff) {
            if (heightDiff != 0) {
                if (mNextHeight == 0) {
                    mNextHeight = mHeight;
                }
                mNextHeight += heightDiff;
                if (mAnimator != null) {
                    mAnimator.cancel();
                }
                mAnimator = ObjectAnimator.ofInt(this, "height", mHeight, mNextHeight);
                mAnimator.setStartDelay(SIZE_START_DELAY);
                mAnimator.setDuration(SIZE_START_LENGTH);
                mAnimator.start();
            }
        }

        public void setHeight(int height) {
            mHeight = height;
            updateHeight();
        }

        @Override
        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
                int oldTop, int oldRight, int oldBottom) {
            // Ensure that the height never changes.
            updateHeight();
        }

        private void updateHeight() {
            if (mControlTop) {
                mView.setTop(mView.getBottom() - mHeight);
            } else {
                mView.setBottom(mView.getTop() + mHeight);
            }
        }
    }
}
