/*
 * Copyright (C) 2017 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 static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE;
import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE;
import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.provider.Settings;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;

import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;
import com.android.systemui.util.leak.RotationUtils;

/**
 * Layout for placing two containers at a specific physical position on the device, relative to the
 * device's hardware, regardless of screen rotation.
 */
public class HardwareUiLayout extends MultiListLayout implements Tunable {

    private static final String EDGE_BLEED = "sysui_hwui_edge_bleed";
    private static final String ROUNDED_DIVIDER = "sysui_hwui_rounded_divider";
    private final int[] mTmp2 = new int[2];
    private ViewGroup mList;
    private ViewGroup mSeparatedView;
    private int mOldHeight;
    private boolean mAnimating;
    private AnimatorSet mAnimation;
    private View mDivision;
    private HardwareBgDrawable mListBackground;
    private HardwareBgDrawable mSeparatedViewBackground;
    private Animator mAnimator;
    private boolean mCollapse;
    private int mEndPoint;
    private boolean mEdgeBleed;
    private boolean mRoundedDivider;
    private boolean mRotatedBackground;
    private boolean mSwapOrientation = true;

    public HardwareUiLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // Manually re-initialize mRotation to portrait-mode, since this view must always
        // be constructed in portrait mode and rotated into the correct initial position.
        mRotation = ROTATION_NONE;
        updateSettings();
    }

    @Override
    protected ViewGroup getSeparatedView() {
        return findViewById(com.android.systemui.R.id.separated_button);
    }

    @Override
    protected ViewGroup getListView() {
        return findViewById(android.R.id.list);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        updateSettings();
        Dependency.get(TunerService.class).addTunable(this, EDGE_BLEED, ROUNDED_DIVIDER);
        getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getViewTreeObserver().removeOnComputeInternalInsetsListener(mInsetsListener);
        Dependency.get(TunerService.class).removeTunable(this);
    }

    @Override
    public void onTuningChanged(String key, String newValue) {
        updateSettings();
    }

    private void updateSettings() {
        mEdgeBleed = Settings.Secure.getInt(getContext().getContentResolver(),
                EDGE_BLEED, 0) != 0;
        mRoundedDivider = Settings.Secure.getInt(getContext().getContentResolver(),
                ROUNDED_DIVIDER, 0) != 0;
        updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding());
        mListBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed, getContext());
        mSeparatedViewBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed,
                getContext());
        if (mList != null) {
            mList.setBackground(mListBackground);
            mSeparatedView.setBackground(mSeparatedViewBackground);
            requestLayout();
        }
    }

    private void updateEdgeMargin(int edge) {
        if (mList != null) {
            MarginLayoutParams params = (MarginLayoutParams) mList.getLayoutParams();
            if (mRotation == ROTATION_LANDSCAPE) {
                params.topMargin = edge;
            } else if (mRotation == ROTATION_SEASCAPE) {
                params.bottomMargin = edge;
            } else {
                params.rightMargin = edge;
                params.leftMargin = edge;
            }
            mList.setLayoutParams(params);
        }

        if (mSeparatedView != null) {
            MarginLayoutParams params = (MarginLayoutParams) mSeparatedView.getLayoutParams();
            if (mRotation == ROTATION_LANDSCAPE) {
                params.topMargin = edge;
            } else if (mRotation == ROTATION_SEASCAPE) {
                params.bottomMargin = edge;
            } else {
                params.rightMargin = edge;
            }
            mSeparatedView.setLayoutParams(params);
        }
    }

    private int getEdgePadding() {
        return getContext().getResources().getDimensionPixelSize(R.dimen.edge_margin);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mList == null) {
            if (getChildCount() != 0) {
                mList = getListView();
                mList.setBackground(mListBackground);
                mSeparatedView = getSeparatedView();
                mSeparatedView.setBackground(mSeparatedViewBackground);
                updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding());
                mOldHeight = mList.getMeasuredHeight();

                // Must be called to initialize view rotation correctly.
                // Requires LayoutParams, hence why this isn't called during the constructor.
                updateRotation();
            } else {
                return;
            }
        }
        int newHeight = mList.getMeasuredHeight();
        if (newHeight != mOldHeight) {
            animateChild(mOldHeight, newHeight);
        }

        post(() -> updatePaddingAndGravityIfTooTall());
        post(() -> updatePosition());
    }

    public void setSwapOrientation(boolean swapOrientation) {
        mSwapOrientation = swapOrientation;
    }

    private void updateRotation() {
        int rotation = RotationUtils.getRotation(getContext());
        if (rotation != mRotation) {
            rotate(mRotation, rotation);
            mRotation = rotation;
        }
    }

    /**
     *  Requires LayoutParams to be set to work correctly, and therefore must be run after after
     *  the HardwareUILayout has been added to the view hierarchy.
     */
    protected void rotate(int from, int to) {
        super.rotate(from, to);
        if (from != ROTATION_NONE && to != ROTATION_NONE) {
            // Rather than handling this confusing case, just do 2 rotations.
            rotate(from, ROTATION_NONE);
            rotate(ROTATION_NONE, to);
            return;
        }
        if (from == ROTATION_LANDSCAPE || to == ROTATION_SEASCAPE) {
            rotateRight();
        } else {
            rotateLeft();
        }
        if (mAdapter.hasSeparatedItems()) {
            if (from == ROTATION_SEASCAPE || to == ROTATION_SEASCAPE) {
                // Separated view has top margin, so seascape separated view need special rotation,
                // not a full left or right rotation.
                swapLeftAndTop(mSeparatedView);
            } else if (from == ROTATION_LANDSCAPE) {
                rotateRight(mSeparatedView);
            } else {
                rotateLeft(mSeparatedView);
            }
        }
        if (to != ROTATION_NONE) {
            if (mList instanceof LinearLayout) {
                mRotatedBackground = true;
                mListBackground.setRotatedBackground(true);
                mSeparatedViewBackground.setRotatedBackground(true);
                LinearLayout linearLayout = (LinearLayout) mList;
                if (mSwapOrientation) {
                    linearLayout.setOrientation(LinearLayout.HORIZONTAL);
                    setOrientation(LinearLayout.HORIZONTAL);
                }
                swapDimens(mList);
                swapDimens(mSeparatedView);
            }
        } else {
            if (mList instanceof LinearLayout) {
                mRotatedBackground = false;
                mListBackground.setRotatedBackground(false);
                mSeparatedViewBackground.setRotatedBackground(false);
                LinearLayout linearLayout = (LinearLayout) mList;
                if (mSwapOrientation) {
                    linearLayout.setOrientation(LinearLayout.VERTICAL);
                    setOrientation(LinearLayout.VERTICAL);
                }
                swapDimens(mList);
                swapDimens(mSeparatedView);
            }
        }
    }

    @Override
    public void onUpdateList() {
        super.onUpdateList();

        for (int i = 0; i < mAdapter.getCount(); i++) {
            ViewGroup parent;
            boolean separated = mAdapter.shouldBeSeparated(i);
            if (separated) {
                parent = getSeparatedView();
            } else {
                parent = getListView();
            }
            View v = mAdapter.getView(i, null, parent);
            parent.addView(v);
        }
    }

    private void rotateRight() {
        rotateRight(this);
        rotateRight(mList);
        swapDimens(this);

        LayoutParams p = (LayoutParams) mList.getLayoutParams();
        p.gravity = rotateGravityRight(p.gravity);
        mList.setLayoutParams(p);

        LayoutParams separatedViewLayoutParams = (LayoutParams) mSeparatedView.getLayoutParams();
        separatedViewLayoutParams.gravity = rotateGravityRight(separatedViewLayoutParams.gravity);
        mSeparatedView.setLayoutParams(separatedViewLayoutParams);

        setGravity(rotateGravityRight(getGravity()));
    }

    private void swapDimens(View v) {
        ViewGroup.LayoutParams params = v.getLayoutParams();
        int h = params.width;
        params.width = params.height;
        params.height = h;
        v.setLayoutParams(params);
    }

    private int rotateGravityRight(int gravity) {
        int retGravity = 0;
        int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
        final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                retGravity |= Gravity.CENTER_VERTICAL;
                break;
            case Gravity.RIGHT:
                retGravity |= Gravity.BOTTOM;
                break;
            case Gravity.LEFT:
            default:
                retGravity |= Gravity.TOP;
                break;
        }

        switch (verticalGravity) {
            case Gravity.CENTER_VERTICAL:
                retGravity |= Gravity.CENTER_HORIZONTAL;
                break;
            case Gravity.BOTTOM:
                retGravity |= Gravity.LEFT;
                break;
            case Gravity.TOP:
            default:
                retGravity |= Gravity.RIGHT;
                break;
        }
        return retGravity;
    }

    private void rotateLeft() {
        rotateLeft(this);
        rotateLeft(mList);
        swapDimens(this);

        LayoutParams p = (LayoutParams) mList.getLayoutParams();
        p.gravity = rotateGravityLeft(p.gravity);
        mList.setLayoutParams(p);

        LayoutParams separatedViewLayoutParams = (LayoutParams) mSeparatedView.getLayoutParams();
        separatedViewLayoutParams.gravity = rotateGravityLeft(separatedViewLayoutParams.gravity);
        mSeparatedView.setLayoutParams(separatedViewLayoutParams);

        setGravity(rotateGravityLeft(getGravity()));
    }

    private int rotateGravityLeft(int gravity) {
        if (gravity == -1) {
            gravity = Gravity.TOP | Gravity.START;
        }
        int retGravity = 0;
        int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
        final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                retGravity |= Gravity.CENTER_VERTICAL;
                break;
            case Gravity.RIGHT:
                retGravity |= Gravity.TOP;
                break;
            case Gravity.LEFT:
            default:
                retGravity |= Gravity.BOTTOM;
                break;
        }

        switch (verticalGravity) {
            case Gravity.CENTER_VERTICAL:
                retGravity |= Gravity.CENTER_HORIZONTAL;
                break;
            case Gravity.BOTTOM:
                retGravity |= Gravity.RIGHT;
                break;
            case Gravity.TOP:
            default:
                retGravity |= Gravity.LEFT;
                break;
        }
        return retGravity;
    }

    private void rotateLeft(View v) {
        v.setPadding(v.getPaddingTop(), v.getPaddingRight(), v.getPaddingBottom(),
                v.getPaddingLeft());
        MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
        params.setMargins(params.topMargin, params.rightMargin, params.bottomMargin,
                params.leftMargin);
        v.setLayoutParams(params);
    }

    private void rotateRight(View v) {
        v.setPadding(v.getPaddingBottom(), v.getPaddingLeft(), v.getPaddingTop(),
                v.getPaddingRight());
        MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
        params.setMargins(params.bottomMargin, params.leftMargin, params.topMargin,
                params.rightMargin);
        v.setLayoutParams(params);
    }

    private void swapLeftAndTop(View v) {
        v.setPadding(v.getPaddingTop(), v.getPaddingLeft(), v.getPaddingBottom(),
                v.getPaddingRight());
        MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
        params.setMargins(params.topMargin, params.leftMargin, params.bottomMargin,
                params.rightMargin);
        v.setLayoutParams(params);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        post(() -> updatePosition());

    }

    private void animateChild(int oldHeight, int newHeight) {
        if (true) return;
        if (mAnimating) {
            mAnimation.cancel();
        }
        mAnimating = true;
        mAnimation = new AnimatorSet();
        mAnimation.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mAnimating = false;
            }
        });
        int fromTop = mList.getTop();
        int fromBottom = mList.getBottom();
        int toTop = fromTop - ((newHeight - oldHeight) / 2);
        int toBottom = fromBottom + ((newHeight - oldHeight) / 2);
        ObjectAnimator top = ObjectAnimator.ofInt(mList, "top", fromTop, toTop);
        top.addUpdateListener(animation -> mListBackground.invalidateSelf());
        mAnimation.playTogether(top,
                ObjectAnimator.ofInt(mList, "bottom", fromBottom, toBottom));
    }

    public void setDivisionView(View v) {
        mDivision = v;
        if (mDivision != null) {
            mDivision.addOnLayoutChangeListener(
                    (v1, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
                            updatePosition());
        }
        updatePosition();
    }

    private void updatePosition() {
        if (mList == null) return;
        // If got separated button, setRotatedBackground to false,
        // all items won't get white background.
        boolean separated = mAdapter.hasSeparatedItems();
        mListBackground.setRotatedBackground(separated);
        mSeparatedViewBackground.setRotatedBackground(separated);
        if (mDivision != null && mDivision.getVisibility() == VISIBLE) {
            int index = mRotatedBackground ? 0 : 1;
            mDivision.getLocationOnScreen(mTmp2);
            float trans = mRotatedBackground ? mDivision.getTranslationX()
                    : mDivision.getTranslationY();
            int viewTop = (int) (mTmp2[index] + trans);
            mList.getLocationOnScreen(mTmp2);
            viewTop -= mTmp2[index];
            setCutPoint(viewTop);
        } else {
            setCutPoint(mList.getMeasuredHeight());
        }
    }

    private void setCutPoint(int point) {
        int curPoint = mListBackground.getCutPoint();
        if (curPoint == point) return;
        if (getAlpha() == 0 || curPoint == 0) {
            mListBackground.setCutPoint(point);
            return;
        }
        if (mAnimator != null) {
            if (mEndPoint == point) {
                return;
            }
            mAnimator.cancel();
        }
        mEndPoint = point;
        mAnimator = ObjectAnimator.ofInt(mListBackground, "cutPoint", curPoint, point);
        if (mCollapse) {
            mAnimator.setStartDelay(300);
            mCollapse = false;
        }
        mAnimator.start();
    }

    // If current power menu height larger then screen height, remove padding to break power menu
    // alignment and set menu center vertical within the screen.
    private void updatePaddingAndGravityIfTooTall() {
        int defaultTopPadding;
        int viewsTotalHeight;
        int separatedViewTopMargin;
        int screenHeight;
        int totalHeight;
        int targetGravity;
        boolean separated = mAdapter.hasSeparatedItems();
        MarginLayoutParams params = (MarginLayoutParams) mSeparatedView.getLayoutParams();
        switch (RotationUtils.getRotation(getContext())) {
            case RotationUtils.ROTATION_LANDSCAPE:
                defaultTopPadding = getPaddingLeft();
                viewsTotalHeight = mList.getMeasuredWidth() + mSeparatedView.getMeasuredWidth();
                separatedViewTopMargin = separated ? params.leftMargin : 0;
                screenHeight = getMeasuredWidth();
                targetGravity = Gravity.CENTER_HORIZONTAL|Gravity.TOP;
                break;
            case RotationUtils.ROTATION_SEASCAPE:
                defaultTopPadding = getPaddingRight();
                viewsTotalHeight = mList.getMeasuredWidth() + mSeparatedView.getMeasuredWidth();
                separatedViewTopMargin = separated ? params.leftMargin : 0;
                screenHeight = getMeasuredWidth();
                targetGravity = Gravity.CENTER_HORIZONTAL|Gravity.BOTTOM;
                break;
            default: // Portrait
                defaultTopPadding = getPaddingTop();
                viewsTotalHeight = mList.getMeasuredHeight() + mSeparatedView.getMeasuredHeight();
                separatedViewTopMargin = separated ? params.topMargin : 0;
                screenHeight = getMeasuredHeight();
                targetGravity = Gravity.CENTER_VERTICAL|Gravity.RIGHT;
                break;
        }
        totalHeight = defaultTopPadding + viewsTotalHeight + separatedViewTopMargin;
        if (totalHeight >= screenHeight) {
            setPadding(0, 0, 0, 0);
            setGravity(targetGravity);
        }
    }

    @Override
    public ViewOutlineProvider getOutlineProvider() {
        return super.getOutlineProvider();
    }

    public void setCollapse() {
        mCollapse = true;
    }

    private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = inoutInfo -> {
        if (mHasOutsideTouch || (mList == null)) {
            inoutInfo.setTouchableInsets(
                    ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
            return;
        }
        inoutInfo.setTouchableInsets(
                ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT);
        inoutInfo.contentInsets.set(mList.getLeft(), mList.getTop(),
                0, getBottom() - mList.getBottom());
    };

    private float getAnimationDistance() {
        return getContext().getResources().getDimension(
                com.android.systemui.R.dimen.global_actions_panel_width) / 2;
    }

    @Override
    public float getAnimationOffsetX() {
        if (RotationUtils.getRotation(mContext) == ROTATION_NONE) {
            return getAnimationDistance();
        }
        return 0;
    }

    @Override
    public float getAnimationOffsetY() {
        switch (RotationUtils.getRotation(getContext())) {
            case RotationUtils.ROTATION_LANDSCAPE:
                return -getAnimationDistance();
            case RotationUtils.ROTATION_SEASCAPE:
                return getAnimationDistance();
            default: // Portrait
                return 0;
        }
    }
}
