/*
 * 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.view;

import static android.app.Flags.notificationsRedesignTemplates;

import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.os.Trace;
import android.util.AttributeSet;
import android.widget.RemoteViews;

import com.android.internal.R;

import java.util.HashSet;
import java.util.Set;

/**
 * The top line of content in a notification view.
 * This includes the text views and badges but excludes the icon and the expander.
 *
 * @hide
 */
@RemoteViews.RemoteView
public class NotificationTopLineView extends ViewGroup {
    private final OverflowAdjuster mOverflowAdjuster = new OverflowAdjuster();
    private final int mGravityY;
    private final int mChildMinWidth;
    private final int mChildHideWidth;
    @Nullable private View mAppName;
    @Nullable private View mTitle;
    private View mHeaderText;
    private View mHeaderTextDivider;
    private View mSecondaryHeaderText;
    private View mSecondaryHeaderTextDivider;
    private OnClickListener mFeedbackListener;
    private HeaderTouchListener mTouchListener = new HeaderTouchListener();
    private View mFeedbackIcon;
    private int mHeaderTextMarginEnd;

    private Set<View> mViewsToDisappear = new HashSet<>();

    private int mMaxAscent;
    private int mMaxDescent;

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

    public NotificationTopLineView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NotificationTopLineView(Context context, @Nullable AttributeSet attrs,
            int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public NotificationTopLineView(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        Resources res = getResources();
        mChildMinWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_min_width);
        mChildHideWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_hide_width);

        // NOTE: Implementation only supports TOP, BOTTOM, and CENTER_VERTICAL gravities,
        // with CENTER_VERTICAL being the default.
        int[] attrIds = {android.R.attr.gravity};
        TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes);
        int gravity = ta.getInt(0, 0);
        ta.recycle();
        if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) {
            mGravityY = Gravity.BOTTOM;
        } else if ((gravity & Gravity.TOP) == Gravity.TOP) {
            mGravityY = Gravity.TOP;
        } else {
            mGravityY = Gravity.CENTER_VERTICAL;
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mAppName = findViewById(R.id.app_name_text);
        mTitle = findViewById(R.id.title);
        mHeaderText = findViewById(R.id.header_text);
        mHeaderTextDivider = findViewById(R.id.header_text_divider);
        mSecondaryHeaderText = findViewById(R.id.header_text_secondary);
        mSecondaryHeaderTextDivider = findViewById(R.id.header_text_secondary_divider);
        mFeedbackIcon = findViewById(R.id.feedback);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Trace.beginSection("NotificationTopLineView#onMeasure");
        final int givenWidth = MeasureSpec.getSize(widthMeasureSpec);
        final int givenHeight = MeasureSpec.getSize(heightMeasureSpec);
        final boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST;
        int wrapContentWidthSpec = MeasureSpec.makeMeasureSpec(givenWidth, MeasureSpec.AT_MOST);
        int heightSpec = notificationsRedesignTemplates()
                ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
                : MeasureSpec.makeMeasureSpec(givenHeight, MeasureSpec.AT_MOST);
        int totalWidth = getPaddingStart();
        int maxChildHeight = -1;
        mMaxAscent = -1;
        mMaxDescent = -1;
        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                // We'll give it the rest of the space in the end
                continue;
            }
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidthSpec = getChildMeasureSpec(wrapContentWidthSpec,
                    lp.leftMargin + lp.rightMargin, lp.width);
            int childHeightSpec = getChildMeasureSpec(heightSpec,
                    lp.topMargin + lp.bottomMargin, lp.height);
            child.measure(childWidthSpec, childHeightSpec);
            totalWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
            int childBaseline = child.getBaseline();
            int childHeight = child.getMeasuredHeight();
            if (childBaseline != -1) {
                mMaxAscent = Math.max(mMaxAscent, childBaseline);
                mMaxDescent = Math.max(mMaxDescent, childHeight - childBaseline);
            }
            maxChildHeight = Math.max(maxChildHeight, childHeight);
        }

        mViewsToDisappear.clear();
        // Ensure that there is at least enough space for the icons
        int endMargin = Math.max(mHeaderTextMarginEnd, getPaddingEnd());
        if (totalWidth > givenWidth - endMargin) {
            int overFlow = totalWidth - givenWidth + endMargin;

            mOverflowAdjuster.resetForOverflow(overFlow, heightSpec)
                    // First shrink the app name, down to a minimum size
                    .adjust(mAppName, null, mChildMinWidth)
                    // Next, shrink the header text (this usually has subText)
                    //   This shrinks the subtext first, but not all the way (yet!)
                    .adjust(mHeaderText, mHeaderTextDivider, mChildMinWidth)
                    // Next, shrink the secondary header text  (this rarely has conversationTitle)
                    .adjust(mSecondaryHeaderText, mSecondaryHeaderTextDivider, 0)
                    // Next, shrink the title text (this has contentTitle; only in headerless views)
                    .adjust(mTitle, null, mChildMinWidth)
                    // Next, shrink the header down to 0 if still necessary.
                    .adjust(mHeaderText, mHeaderTextDivider, 0)
                    // Finally, shrink the title to 0 if necessary (media is super cramped)
                    .adjust(mTitle, null, 0)
                    // Clean up
                    .finish();
        }
        setMeasuredDimension(givenWidth, wrapHeight ? maxChildHeight : givenHeight);
        Trace.endSection();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
        final int width = getWidth();
        int start = getPaddingStart();
        int childCount = getChildCount();
        int ownHeight = b - t;
        int childSpace = ownHeight - mPaddingTop - mPaddingBottom;

        // Instead of centering the baseline, pick a baseline that centers views which align to it.
        // Only used when mGravityY is CENTER_VERTICAL
        int baselineY = mPaddingTop + ((childSpace - (mMaxAscent + mMaxDescent)) / 2) + mMaxAscent;

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            int childHeight = child.getMeasuredHeight();
            MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();

            // Calculate vertical alignment of the views, accounting for the view baselines
            int childTop;
            int childBaseline = child.getBaseline();
            switch (mGravityY) {
                case Gravity.TOP:
                    childTop = mPaddingTop + params.topMargin;
                    if (childBaseline != -1) {
                        childTop += mMaxAscent - childBaseline;
                    }
                    break;
                case Gravity.CENTER_VERTICAL:
                    if (childBaseline != -1) {
                        // Align baselines vertically only if the child is smaller than us
                        if (childSpace - childHeight > 0) {
                            childTop = baselineY - childBaseline;
                        } else {
                            childTop = mPaddingTop + (childSpace - childHeight) / 2;
                        }
                    } else {
                        childTop = mPaddingTop + ((childSpace - childHeight) / 2)
                                + params.topMargin - params.bottomMargin;
                    }
                    break;
                case Gravity.BOTTOM:
                    int childBottom = ownHeight - mPaddingBottom;
                    childTop = childBottom - childHeight - params.bottomMargin;
                    if (childBaseline != -1) {
                        int descent = childHeight - childBaseline;
                        childTop -= (mMaxDescent - descent);
                    }
                    break;
                default:
                    childTop = mPaddingTop;
            }
            if (mViewsToDisappear.contains(child)) {
                child.layout(start, childTop, start, childTop + childHeight);
            } else {
                start += params.getMarginStart();
                int end = start + child.getMeasuredWidth();
                int layoutLeft = isRtl ? width - end : start;
                int layoutRight = isRtl ? width - start : end;
                start = end + params.getMarginEnd();
                child.layout(layoutLeft, childTop, layoutRight, childTop + childHeight);
            }
        }
        updateTouchListener();
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    private void updateTouchListener() {
        if (mFeedbackListener == null) {
            setOnTouchListener(null);
            return;
        }
        setOnTouchListener(mTouchListener);
        mTouchListener.bindTouchRects();
    }

    /**
     * Sets onclick listener for feedback icon.
     */
    public void setFeedbackOnClickListener(OnClickListener l) {
        mFeedbackListener = l;
        mFeedbackIcon.setOnClickListener(mFeedbackListener);
        updateTouchListener();
    }

    /**
     * Sets the margin end for the text portion of the header, excluding right-aligned elements
     *
     * @param headerTextMarginEnd margin size
     */
    public void setHeaderTextMarginEnd(int headerTextMarginEnd) {
        if (mHeaderTextMarginEnd != headerTextMarginEnd) {
            mHeaderTextMarginEnd = headerTextMarginEnd;
            requestLayout();
        }
    }

    /**
     * Get the current margin end value for the header text
     *
     * @return margin size
     */
    public int getHeaderTextMarginEnd() {
        return mHeaderTextMarginEnd;
    }

    /**
     * Set padding at the start of the view.
     */
    public void setPaddingStart(int paddingStart) {
        setPaddingRelative(paddingStart, getPaddingTop(), getPaddingEnd(), getPaddingBottom());
    }

    private class HeaderTouchListener implements OnTouchListener {

        private Rect mFeedbackRect;
        private int mTouchSlop;
        private boolean mTrackGesture;
        private float mDownX;
        private float mDownY;

        HeaderTouchListener() {
        }

        public void bindTouchRects() {
            mFeedbackRect = getRectAroundView(mFeedbackIcon);
            mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        }

        private Rect getRectAroundView(View view) {
            float size = 48 * getResources().getDisplayMetrics().density;
            float width = Math.max(size, view.getWidth());
            float height = Math.max(size, view.getHeight());
            final Rect r = new Rect();
            if (view.getVisibility() == GONE) {
                view = getFirstChildNotGone();
                r.left = (int) (view.getLeft() - width / 2.0f);
            } else {
                r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f);
            }
            r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f);
            r.bottom = (int) (r.top + height);
            r.right = (int) (r.left + width);
            return r;
        }

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            float x = event.getX();
            float y = event.getY();
            switch (event.getActionMasked() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    mTrackGesture = false;
                    if (isInside(x, y)) {
                        mDownX = x;
                        mDownY = y;
                        mTrackGesture = true;
                        return true;
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (mTrackGesture) {
                        if (Math.abs(mDownX - x) > mTouchSlop
                                || Math.abs(mDownY - y) > mTouchSlop) {
                            mTrackGesture = false;
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    if (mTrackGesture && onTouchUp(x, y, mDownX, mDownY)) {
                        return true;
                    }
                    break;
            }
            return mTrackGesture;
        }

        private boolean onTouchUp(float upX, float upY, float downX, float downY) {
            if (mFeedbackIcon.isVisibleToUser()
                    && (mFeedbackRect.contains((int) upX, (int) upY)
                    || mFeedbackRect.contains((int) downX, (int) downY))) {
                mFeedbackIcon.performClick();
                return true;
            }
            return false;
        }

        private boolean isInside(float x, float y) {
            return mFeedbackRect.contains((int) x, (int) y);
        }
    }

    private View getFirstChildNotGone() {
        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                return child;
            }
        }
        return this;
    }

    @Override
    public boolean hasOverlappingRendering() {
        return false;
    }

    /**
     * Returns whether the title is present.
     */
    public boolean isTitlePresent() {
        return mTitle != null;
    }

    /**
     * Determine if the given point is touching an active part of the top line.
     */
    public boolean isInTouchRect(float x, float y) {
        if (mFeedbackListener == null) {
            return false;
        }
        return mTouchListener.isInside(x, y);
    }

    /**
     * Perform a click on an active part of the top line, if touching.
     */
    public boolean onTouchUp(float upX, float upY, float downX, float downY) {
        if (mFeedbackListener == null) {
            return false;
        }
        return mTouchListener.onTouchUp(upX, upY, downX, downY);
    }

    private final class OverflowAdjuster {
        private int mOverflow;
        private int mHeightSpec;
        private View mRegrowView;

        OverflowAdjuster resetForOverflow(int overflow, int heightSpec) {
            mOverflow = overflow;
            mHeightSpec = heightSpec;
            mRegrowView = null;
            return this;
        }

        /**
         * Shrink the targetView's width by up to overFlow, down to minimumWidth.
         * @param targetView the view to shrink the width of
         * @param targetDivider a divider view which should be set to 0 width if the targetView is
         * @param minimumWidth the minimum width allowed for the targetView
         * @return this object
         */
        OverflowAdjuster adjust(View targetView, View targetDivider, int minimumWidth) {
            if (mOverflow <= 0 || targetView == null || targetView.getVisibility() == View.GONE) {
                return this;
            }
            final int oldWidth = targetView.getMeasuredWidth();
            if (oldWidth <= minimumWidth) {
                return this;
            }
            // we're too big
            int newSize = Math.max(minimumWidth, oldWidth - mOverflow);
            if (minimumWidth == 0 && newSize < mChildHideWidth
                    && mRegrowView != null && mRegrowView != targetView) {
                // View is so small it's better to hide it entirely (and its divider and margins)
                // so we can give that space back to another previously shrunken view.
                newSize = 0;
            }

            int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
            targetView.measure(childWidthSpec, mHeightSpec);
            mOverflow -= oldWidth - newSize;

            if (newSize == 0) {
                mViewsToDisappear.add(targetView);
                mOverflow -= getHorizontalMargins(targetView);
                if (targetDivider != null && targetDivider.getVisibility() != GONE) {
                    mViewsToDisappear.add(targetDivider);
                    int oldDividerWidth = targetDivider.getMeasuredWidth();
                    int dividerWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.AT_MOST);
                    targetDivider.measure(dividerWidthSpec, mHeightSpec);
                    mOverflow -= (oldDividerWidth + getHorizontalMargins(targetDivider));
                }
            }
            if (mOverflow < 0 && mRegrowView != null) {
                // We're now under-flowing, so regrow the last view.
                final int regrowCurrentSize = mRegrowView.getMeasuredWidth();
                final int maxSize = regrowCurrentSize - mOverflow;
                int regrowWidthSpec = MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST);
                mRegrowView.measure(regrowWidthSpec, mHeightSpec);
                finish();
                return this;
            }

            if (newSize != 0) {
                // if we shrunk this view (but did not completely hide it) store it for potential
                // re-growth if we proactively shorten a future view.
                mRegrowView = targetView;
            }
            return this;
        }

        void finish() {
            resetForOverflow(0, 0);
        }

        private int getHorizontalMargins(View view) {
            MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams();
            return params.getMarginStart() + params.getMarginEnd();
        }
    }
}
