/*
 * Copyright (C) 2022 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.settings.fuelgauge.batteryusage;

import static com.android.settings.Utils.formatPercentage;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_ALL;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_INVALID;
import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;

import static java.lang.Math.abs;
import static java.lang.Math.round;
import static java.util.Objects.requireNonNull;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.widget.AppCompatImageView;

import com.android.settings.R;
import com.android.settingslib.Utils;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;

/** A widget component to draw chart graph. */
public class BatteryChartView extends AppCompatImageView implements View.OnClickListener {
    private static final String TAG = "BatteryChartView";

    private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
    private static final int HORIZONTAL_DIVIDER_COUNT = 5;

    /** A callback listener for selected group index is updated. */
    public interface OnSelectListener {
        /** The callback function for selected group index is updated. */
        void onSelect(int trapezoidIndex);
    }

    private final String[] mPercentages = getPercentages();
    private final Rect mIndent = new Rect();
    private final Rect[] mPercentageBounds = new Rect[] {new Rect(), new Rect(), new Rect()};
    private final List<Rect> mAxisLabelsBounds = new ArrayList<>();
    private final Set<Integer> mLabelDrawnIndexes = new ArraySet<>();
    private final int mLayoutDirection =
            getContext().getResources().getConfiguration().getLayoutDirection();

    private BatteryChartViewModel mViewModel;
    private int mHoveredIndex = SELECTED_INDEX_INVALID;
    private int mDividerWidth;
    private int mDividerHeight;
    private float mTrapezoidVOffset;
    private float mTrapezoidHOffset;
    private int mTrapezoidColor;
    private int mTrapezoidSolidColor;
    private int mTrapezoidHoverColor;
    private int mDefaultTextColor;
    private int mTextPadding;
    private int mTransomIconSize;
    private int mTransomTop;
    private int mTransomViewHeight;
    private int mTransomLineDefaultColor;
    private int mTransomLineSelectedColor;
    private float mTransomPadding;
    private Drawable mTransomIcon;
    private Paint mTransomLinePaint;
    private Paint mTransomSelectedSlotPaint;
    private Paint mDividerPaint;
    private Paint mTrapezoidPaint;
    private Paint mTextPaint;
    private AccessibilityNodeProvider mAccessibilityNodeProvider;
    private BatteryChartView.OnSelectListener mOnSelectListener;

    @VisibleForTesting TrapezoidSlot[] mTrapezoidSlots;
    // Records the location to calculate selected index.
    @VisibleForTesting float mTouchUpEventX = Float.MIN_VALUE;

    public BatteryChartView(Context context) {
        super(context, null);
    }

    public BatteryChartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initializeColors(context);
        // Registers the click event listener.
        setOnClickListener(this);
        setClickable(false);
        requestLayout();
    }

    /** Sets the data model of this view. */
    public void setViewModel(BatteryChartViewModel viewModel) {
        if (viewModel == null) {
            mViewModel = null;
            invalidate();
            return;
        }

        Log.d(
                TAG,
                String.format(
                        "setViewModel(): size: %d, selectedIndex: %d, getHighlightSlotIndex: %d",
                        viewModel.size(),
                        viewModel.selectedIndex(),
                        viewModel.getHighlightSlotIndex()));
        mViewModel = viewModel;
        initializeAxisLabelsBounds();
        initializeTrapezoidSlots(viewModel.size() - 1);
        setClickable(hasAnyValidTrapezoid(viewModel));
        requestLayout();
    }

    /** Sets the callback to monitor the selected group index. */
    public void setOnSelectListener(BatteryChartView.OnSelectListener listener) {
        mOnSelectListener = listener;
    }

    /** Sets the companion {@link TextView} for percentage information. */
    public void setCompanionTextView(TextView textView) {
        if (textView != null) {
            // Pre-draws the view first to load style atttributions into paint.
            textView.draw(new Canvas());
            mTextPaint = textView.getPaint();
            mDefaultTextColor = mTextPaint.getColor();
        } else {
            mTextPaint = null;
        }
        requestLayout();
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // Measures text bounds and updates indent configuration.
        if (mTextPaint != null) {
            mTextPaint.setTextAlign(Paint.Align.LEFT);
            for (int index = 0; index < mPercentages.length; index++) {
                mTextPaint.getTextBounds(
                        mPercentages[index],
                        0,
                        mPercentages[index].length(),
                        mPercentageBounds[index]);
            }
            // Updates the indent configurations.
            mIndent.top = mPercentageBounds[0].height() + mTransomViewHeight;
            final int textWidth = mPercentageBounds[0].width() + mTextPadding;
            if (isRTL()) {
                mIndent.left = textWidth;
            } else {
                mIndent.right = textWidth;
            }

            if (mViewModel != null) {
                int maxTop = 0;
                for (int index = 0; index < mViewModel.size(); index++) {
                    final String text = mViewModel.getText(index);
                    mTextPaint.getTextBounds(text, 0, text.length(), mAxisLabelsBounds.get(index));
                    maxTop = Math.max(maxTop, -mAxisLabelsBounds.get(index).top);
                }
                mIndent.bottom = maxTop + round(mTextPadding * 2f);
            }
            Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
        } else {
            mIndent.set(0, 0, 0, 0);
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        // Before mLevels initialized, the count of trapezoids is unknown. Only draws the
        // horizontal percentages and dividers.
        drawHorizontalDividers(canvas);
        if (mViewModel == null) {
            return;
        }
        drawVerticalDividers(canvas);
        drawTrapezoids(canvas);
        drawTransomLine(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Caches the location to calculate selected trapezoid index.
        final int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_UP:
                mTouchUpEventX = event.getX();
                break;
            case MotionEvent.ACTION_CANCEL:
                mTouchUpEventX = Float.MIN_VALUE; // reset
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onHoverEvent(MotionEvent event) {
        final int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_HOVER_ENTER:
            case MotionEvent.ACTION_HOVER_MOVE:
                final int trapezoidIndex = getTrapezoidIndex(event.getX());
                if (mHoveredIndex != trapezoidIndex) {
                    mHoveredIndex = trapezoidIndex;
                    invalidate();
                    sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
                }
                // Ignore the super.onHoverEvent() because the hovered trapezoid has already been
                // sent here.
                return true;
            case MotionEvent.ACTION_HOVER_EXIT:
                if (mHoveredIndex != SELECTED_INDEX_INVALID) {
                    sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
                    mHoveredIndex = SELECTED_INDEX_INVALID; // reset
                    invalidate();
                }
                // Ignore the super.onHoverEvent() because the hovered trapezoid has already been
                // sent here.
                return true;
            default:
                return super.onTouchEvent(event);
        }
    }

    @Override
    public void onHoverChanged(boolean hovered) {
        super.onHoverChanged(hovered);
        if (!hovered) {
            mHoveredIndex = SELECTED_INDEX_INVALID; // reset
            invalidate();
        }
    }

    @Override
    public void onClick(View view) {
        if (mTouchUpEventX == Float.MIN_VALUE) {
            Log.w(TAG, "invalid motion event for onClick() callback");
            return;
        }
        onTrapezoidClicked(view, getTrapezoidIndex(mTouchUpEventX));
    }

    @Override
    public AccessibilityNodeProvider getAccessibilityNodeProvider() {
        if (mViewModel == null) {
            return super.getAccessibilityNodeProvider();
        }
        if (mAccessibilityNodeProvider == null) {
            mAccessibilityNodeProvider = new BatteryChartAccessibilityNodeProvider();
        }
        return mAccessibilityNodeProvider;
    }

    private void onTrapezoidClicked(View view, int index) {
        // Ignores the click event if the level is zero.
        if (!isValidToDraw(mViewModel, index)) {
            return;
        }
        if (mOnSelectListener != null) {
            // Selects all if users click the same trapezoid item two times.
            mOnSelectListener.onSelect(
                    index == mViewModel.selectedIndex() ? SELECTED_INDEX_ALL : index);
        }
        view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
    }

    private boolean sendAccessibilityEvent(int virtualDescendantId, int eventType) {
        ViewParent parent = getParent();
        if (parent == null || !AccessibilityManager.getInstance(mContext).isEnabled()) {
            return false;
        }
        AccessibilityEvent accessibilityEvent = new AccessibilityEvent(eventType);
        accessibilityEvent.setSource(this, virtualDescendantId);
        accessibilityEvent.setEnabled(true);
        accessibilityEvent.setClassName(getAccessibilityClassName());
        accessibilityEvent.setPackageName(getContext().getPackageName());
        return parent.requestSendAccessibilityEvent(this, accessibilityEvent);
    }

    private void sendAccessibilityEventForHover(int eventType) {
        if (isTrapezoidIndexValid(mViewModel, mHoveredIndex)) {
            sendAccessibilityEvent(mHoveredIndex, eventType);
        }
    }

    private void initializeTrapezoidSlots(int count) {
        mTrapezoidSlots = new TrapezoidSlot[count];
        for (int index = 0; index < mTrapezoidSlots.length; index++) {
            mTrapezoidSlots[index] = new TrapezoidSlot();
        }
    }

    private void initializeColors(Context context) {
        setBackgroundColor(Color.TRANSPARENT);
        mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
        mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
        mTrapezoidHoverColor =
                context.getColor(com.android.internal.R.color.materialColorSecondaryContainer);
        // Initializes the divider line paint.
        final Resources resources = getContext().getResources();
        mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
        mDividerHeight = resources.getDimensionPixelSize(R.dimen.chartview_divider_height);
        mDividerPaint = new Paint();
        mDividerPaint.setAntiAlias(true);
        mDividerPaint.setColor(DIVIDER_COLOR);
        mDividerPaint.setStyle(Paint.Style.STROKE);
        mDividerPaint.setStrokeWidth(mDividerWidth);
        Log.i(TAG, "mDividerWidth:" + mDividerWidth);
        Log.i(TAG, "mDividerHeight:" + mDividerHeight);
        // Initializes the trapezoid paint.
        mTrapezoidHOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_start);
        mTrapezoidVOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_bottom);
        mTrapezoidPaint = new Paint();
        mTrapezoidPaint.setAntiAlias(true);
        mTrapezoidPaint.setColor(mTrapezoidSolidColor);
        mTrapezoidPaint.setStyle(Paint.Style.FILL);
        mTrapezoidPaint.setPathEffect(
                new CornerPathEffect(
                        resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius)));
        // Initializes for drawing text information.
        mTextPadding = resources.getDimensionPixelSize(R.dimen.chartview_text_padding);
        // Initializes the padding top for drawing text information.
        mTransomViewHeight =
                resources.getDimensionPixelSize(R.dimen.chartview_transom_layout_height);
    }

    private void initializeTransomPaint() {
        if (mTransomLinePaint != null
                && mTransomSelectedSlotPaint != null
                && mTransomIcon != null) {
            return;
        }
        // Initializes the transom line paint.
        final Resources resources = getContext().getResources();
        final int transomLineWidth =
                resources.getDimensionPixelSize(R.dimen.chartview_transom_width);
        final int transomRadius = resources.getDimensionPixelSize(R.dimen.chartview_transom_radius);
        mTransomPadding = transomRadius * .5f;
        mTransomTop = resources.getDimensionPixelSize(R.dimen.chartview_transom_padding_top);
        mTransomLineDefaultColor = Utils.getDisabled(mContext, DIVIDER_COLOR);
        mTransomLineSelectedColor =
                resources.getColor(
                        com.android.settingslib.widget.preference.banner.R.color
                                .settingslib_banner_button_background_medium);
        final int slotHighlightColor = Utils.getDisabled(mContext, mTransomLineSelectedColor);
        mTransomIconSize = resources.getDimensionPixelSize(R.dimen.chartview_transom_icon_size);
        mTransomLinePaint = new Paint();
        mTransomLinePaint.setAntiAlias(true);
        mTransomLinePaint.setStyle(Paint.Style.STROKE);
        mTransomLinePaint.setStrokeWidth(transomLineWidth);
        mTransomLinePaint.setStrokeCap(Paint.Cap.ROUND);
        mTransomLinePaint.setPathEffect(new CornerPathEffect(transomRadius));
        mTransomSelectedSlotPaint = new Paint();
        mTransomSelectedSlotPaint.setAntiAlias(true);
        mTransomSelectedSlotPaint.setColor(slotHighlightColor);
        mTransomSelectedSlotPaint.setStyle(Paint.Style.FILL);
        // Get the companion icon beside transom line
        mTransomIcon = getResources().getDrawable(R.drawable.ic_battery_tips_warning_icon);
    }

    private void drawHorizontalDividers(Canvas canvas) {
        final int width = getWidth() - abs(mIndent.width());
        final int height = getHeight() - mIndent.top - mIndent.bottom;
        final float topOffsetY = mIndent.top + mDividerWidth * .5f;
        final float bottomOffsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f);
        final float availableSpace = bottomOffsetY - topOffsetY;

        mDividerPaint.setColor(DIVIDER_COLOR);
        final float dividerOffsetUnit = availableSpace / (float) (HORIZONTAL_DIVIDER_COUNT - 1);

        // Draws 5 divider lines.
        for (int index = 0; index < HORIZONTAL_DIVIDER_COUNT; index++) {
            float offsetY = topOffsetY + dividerOffsetUnit * index;
            canvas.drawLine(mIndent.left, offsetY, mIndent.left + width, offsetY, mDividerPaint);

            //  Draws percentage text only for 100% / 50% / 0%
            if (index % 2 == 0) {
                drawPercentage(canvas, /* index= */ (index + 1) / 2, offsetY);
            }
        }
    }

    private void drawPercentage(Canvas canvas, int index, float offsetY) {
        if (mTextPaint != null) {
            mTextPaint.setTextAlign(isRTL() ? Paint.Align.RIGHT : Paint.Align.LEFT);
            mTextPaint.setColor(mDefaultTextColor);
            canvas.drawText(
                    mPercentages[index],
                    isRTL()
                            ? mIndent.left - mTextPadding
                            : getWidth() - mIndent.width() + mTextPadding,
                    offsetY + mPercentageBounds[index].height() * .5f,
                    mTextPaint);
        }
    }

    private void drawVerticalDividers(Canvas canvas) {
        final int width = getWidth() - abs(mIndent.width());
        final int dividerCount = mTrapezoidSlots.length + 1;
        final float dividerSpace = dividerCount * mDividerWidth;
        final float unitWidth = (width - dividerSpace) / (float) mTrapezoidSlots.length;
        final float bottomY = getHeight() - mIndent.bottom;
        final float startY = bottomY - mDividerHeight;
        final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
        // Draws the axis label slot information.
        if (mViewModel != null) {
            final float baselineY = getHeight() - mTextPadding;
            Rect[] axisLabelDisplayAreas;
            switch (mViewModel.axisLabelPosition()) {
                case CENTER_OF_TRAPEZOIDS:
                    axisLabelDisplayAreas =
                            getAxisLabelDisplayAreas(
                                    /* size= */ mViewModel.size() - 1,
                                    /* baselineX= */ mIndent.left + mDividerWidth + unitWidth * .5f,
                                    /* offsetX= */ mDividerWidth + unitWidth,
                                    baselineY,
                                    /* shiftFirstAndLast= */ false);
                    break;
                case BETWEEN_TRAPEZOIDS:
                default:
                    axisLabelDisplayAreas =
                            getAxisLabelDisplayAreas(
                                    /* size= */ mViewModel.size(),
                                    /* baselineX= */ mIndent.left + mDividerWidth * .5f,
                                    /* offsetX= */ mDividerWidth + unitWidth,
                                    baselineY,
                                    /* shiftFirstAndLast= */ true);
                    break;
            }
            drawAxisLabels(canvas, axisLabelDisplayAreas, baselineY);
        }
        // Draws each vertical dividers.
        float startX = mDividerWidth * .5f + mIndent.left;
        for (int index = 0; index < dividerCount; index++) {
            float dividerY = bottomY;
            if (mViewModel.axisLabelPosition() == BETWEEN_TRAPEZOIDS
                    && mLabelDrawnIndexes.contains(index)) {
                mDividerPaint.setColor(mTrapezoidSolidColor);
                dividerY += mDividerHeight / 4f;
            } else {
                mDividerPaint.setColor(DIVIDER_COLOR);
            }
            canvas.drawLine(startX, startY, startX, dividerY, mDividerPaint);
            final float nextX = startX + mDividerWidth + unitWidth;
            // Updates the trapezoid slots for drawing.
            if (index < mTrapezoidSlots.length) {
                final int trapezoidIndex = isRTL() ? mTrapezoidSlots.length - index - 1 : index;
                mTrapezoidSlots[trapezoidIndex].mLeft = round(startX + trapezoidSlotOffset);
                mTrapezoidSlots[trapezoidIndex].mRight = round(nextX - trapezoidSlotOffset);
            }
            startX = nextX;
        }
    }

    /** Gets all the axis label texts displaying area positions if they are shown. */
    private Rect[] getAxisLabelDisplayAreas(
            final int size,
            final float baselineX,
            final float offsetX,
            final float baselineY,
            final boolean shiftFirstAndLast) {
        final Rect[] result = new Rect[size];
        for (int index = 0; index < result.length; index++) {
            final float width = mAxisLabelsBounds.get(index).width();
            float middle = baselineX + index * offsetX;
            if (shiftFirstAndLast) {
                if (index == 0) {
                    middle += width * .5f;
                }
                if (index == size - 1) {
                    middle -= width * .5f;
                }
            }
            final float left = middle - width * .5f;
            final float right = left + width;
            final float top = baselineY + mAxisLabelsBounds.get(index).top;
            final float bottom = top + mAxisLabelsBounds.get(index).height();
            result[index] = new Rect(round(left), round(top), round(right), round(bottom));
        }
        return result;
    }

    private void drawAxisLabels(Canvas canvas, final Rect[] displayAreas, final float baselineY) {
        final int lastIndex = displayAreas.length - 1;
        mLabelDrawnIndexes.clear();
        // Suppose first and last labels are always able to draw.
        drawAxisLabelText(canvas, 0, displayAreas[0], baselineY);
        mLabelDrawnIndexes.add(0);
        drawAxisLabelText(canvas, lastIndex, displayAreas[lastIndex], baselineY);
        mLabelDrawnIndexes.add(lastIndex);
        drawAxisLabelsBetweenStartIndexAndEndIndex(canvas, displayAreas, 0, lastIndex, baselineY);
    }

    /**
     * Recursively draws axis labels between the start index and the end index. If the inner number
     * can be exactly divided into 2 parts, check and draw the middle index label and then
     * recursively draw the 2 parts. Otherwise, divide into 3 parts. Check and draw the middle two
     * labels and then recursively draw the 3 parts. If there are any overlaps, skip drawing and go
     * back to the uplevel of the recursion.
     */
    private void drawAxisLabelsBetweenStartIndexAndEndIndex(
            Canvas canvas,
            final Rect[] displayAreas,
            final int startIndex,
            final int endIndex,
            final float baselineY) {
        if (endIndex - startIndex <= 1) {
            return;
        }
        if ((endIndex - startIndex) % 2 == 0) {
            int middleIndex = (startIndex + endIndex) / 2;
            if (hasOverlap(displayAreas, startIndex, middleIndex)
                    || hasOverlap(displayAreas, middleIndex, endIndex)) {
                return;
            }
            drawAxisLabelText(canvas, middleIndex, displayAreas[middleIndex], baselineY);
            mLabelDrawnIndexes.add(middleIndex);
            drawAxisLabelsBetweenStartIndexAndEndIndex(
                    canvas, displayAreas, startIndex, middleIndex, baselineY);
            drawAxisLabelsBetweenStartIndexAndEndIndex(
                    canvas, displayAreas, middleIndex, endIndex, baselineY);
        } else {
            int middleIndex1 = startIndex + round((endIndex - startIndex) / 3f);
            int middleIndex2 = startIndex + round((endIndex - startIndex) * 2 / 3f);
            if (hasOverlap(displayAreas, startIndex, middleIndex1)
                    || hasOverlap(displayAreas, middleIndex1, middleIndex2)
                    || hasOverlap(displayAreas, middleIndex2, endIndex)) {
                return;
            }
            drawAxisLabelText(canvas, middleIndex1, displayAreas[middleIndex1], baselineY);
            mLabelDrawnIndexes.add(middleIndex1);
            drawAxisLabelText(canvas, middleIndex2, displayAreas[middleIndex2], baselineY);
            mLabelDrawnIndexes.add(middleIndex2);
            drawAxisLabelsBetweenStartIndexAndEndIndex(
                    canvas, displayAreas, startIndex, middleIndex1, baselineY);
            drawAxisLabelsBetweenStartIndexAndEndIndex(
                    canvas, displayAreas, middleIndex1, middleIndex2, baselineY);
            drawAxisLabelsBetweenStartIndexAndEndIndex(
                    canvas, displayAreas, middleIndex2, endIndex, baselineY);
        }
    }

    private boolean hasOverlap(
            final Rect[] displayAreas, final int leftIndex, final int rightIndex) {
        return displayAreas[leftIndex].right + mTextPadding * 2.3f > displayAreas[rightIndex].left;
    }

    private boolean isRTL() {
        return mLayoutDirection == View.LAYOUT_DIRECTION_RTL;
    }

    private void drawAxisLabelText(
            Canvas canvas, int index, final Rect displayArea, final float baselineY) {
        mTextPaint.setColor(mTrapezoidSolidColor);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        // Reverse the sort of axis labels for RTL
        if (isRTL()) {
            index =
                    mViewModel.axisLabelPosition() == BETWEEN_TRAPEZOIDS
                            ? mViewModel.size() - index - 1 // for hourly
                            : mViewModel.size() - index - 2; // for daily
        }
        canvas.drawText(mViewModel.getText(index), displayArea.centerX(), baselineY, mTextPaint);
        mLabelDrawnIndexes.add(index);
    }

    private void drawTrapezoids(Canvas canvas) {
        // Ignores invalid trapezoid data.
        if (mViewModel == null) {
            return;
        }
        final float trapezoidBottom =
                getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth - mTrapezoidVOffset;
        final float availableSpace =
                trapezoidBottom - mDividerWidth * .5f - mIndent.top - mTrapezoidVOffset;
        final float unitHeight = availableSpace / 100f;
        // Draws all trapezoid shapes into the canvas.
        final Path trapezoidPath = new Path();
        Path trapezoidCurvePath = null;
        for (int index = 0; index < mTrapezoidSlots.length; index++) {
            // Not draws the trapezoid for corner or not initialization cases.
            if (!isValidToDraw(mViewModel, index)) {
                continue;
            }
            // Configures the trapezoid paint color.
            final int trapezoidColor =
                    (mViewModel.selectedIndex() == index
                                    || mViewModel.selectedIndex() == SELECTED_INDEX_ALL)
                            ? mTrapezoidSolidColor
                            : mTrapezoidColor;
            final boolean isHoverState =
                    mHoveredIndex == index && isValidToDraw(mViewModel, mHoveredIndex);
            mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);

            float leftTop =
                    round(
                            trapezoidBottom
                                    - requireNonNull(mViewModel.getLevel(index)) * unitHeight);
            float rightTop =
                    round(
                            trapezoidBottom
                                    - requireNonNull(mViewModel.getLevel(index + 1)) * unitHeight);
            // Mirror the shape of the trapezoid for RTL
            if (isRTL()) {
                float temp = leftTop;
                leftTop = rightTop;
                rightTop = temp;
            }
            trapezoidPath.reset();
            trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
            trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
            trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
            trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, trapezoidBottom);
            // A tricky way to make the trapezoid shape drawing the rounded corner.
            trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
            trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
            // Draws the trapezoid shape into canvas.
            canvas.drawPath(trapezoidPath, mTrapezoidPaint);
        }
    }

    private boolean isHighlightSlotValid() {
        return mViewModel != null && mViewModel.getHighlightSlotIndex() != SELECTED_INDEX_INVALID;
    }

    private void drawTransomLine(Canvas canvas) {
        if (!isHighlightSlotValid()) {
            return;
        }
        initializeTransomPaint();
        // Draw the whole transom line and a warning icon
        mTransomLinePaint.setColor(mTransomLineDefaultColor);
        final int width = getWidth() - abs(mIndent.width());
        final float transomOffset = mTrapezoidHOffset + mDividerWidth * .5f + mTransomPadding;
        final float trapezoidBottom =
                getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth - mTrapezoidVOffset;
        canvas.drawLine(
                mIndent.left + transomOffset,
                mTransomTop,
                mIndent.left + width - transomOffset,
                mTransomTop,
                mTransomLinePaint);
        drawTransomIcon(canvas);
        // Draw selected segment of transom line and a highlight slot
        mTransomLinePaint.setColor(mTransomLineSelectedColor);
        final int index = mViewModel.getHighlightSlotIndex();
        final float startX = mTrapezoidSlots[index].mLeft;
        final float endX = mTrapezoidSlots[index].mRight;
        canvas.drawLine(
                startX + mTransomPadding,
                mTransomTop,
                endX - mTransomPadding,
                mTransomTop,
                mTransomLinePaint);
        canvas.drawRect(startX, mTransomTop, endX, trapezoidBottom, mTransomSelectedSlotPaint);
    }

    private void drawTransomIcon(Canvas canvas) {
        if (mTransomIcon == null) {
            return;
        }
        final int left =
                isRTL()
                        ? mIndent.left - mTextPadding - mTransomIconSize
                        : getWidth() - abs(mIndent.width()) + mTextPadding;
        mTransomIcon.setBounds(
                left,
                mTransomTop - mTransomIconSize / 2,
                left + mTransomIconSize,
                mTransomTop + mTransomIconSize / 2);
        mTransomIcon.draw(canvas);
    }

    // Searches the corresponding trapezoid index from x location.
    private int getTrapezoidIndex(float x) {
        if (mTrapezoidSlots == null) {
            return SELECTED_INDEX_INVALID;
        }
        for (int index = 0; index < mTrapezoidSlots.length; index++) {
            final TrapezoidSlot slot = mTrapezoidSlots[index];
            if (x >= slot.mLeft - mTrapezoidHOffset && x <= slot.mRight + mTrapezoidHOffset) {
                return index;
            }
        }
        return SELECTED_INDEX_INVALID;
    }

    private void initializeAxisLabelsBounds() {
        mAxisLabelsBounds.clear();
        for (int i = 0; i < mViewModel.size(); i++) {
            mAxisLabelsBounds.add(new Rect());
        }
    }

    private static boolean isTrapezoidValid(
            @NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
        return viewModel.getLevel(trapezoidIndex) != BATTERY_LEVEL_UNKNOWN
                && viewModel.getLevel(trapezoidIndex + 1) != BATTERY_LEVEL_UNKNOWN;
    }

    private static boolean isTrapezoidIndexValid(
            @NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
        return viewModel != null && trapezoidIndex >= 0 && trapezoidIndex < viewModel.size() - 1;
    }

    private static boolean isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex) {
        return isTrapezoidIndexValid(viewModel, trapezoidIndex)
                && isTrapezoidValid(viewModel, trapezoidIndex);
    }

    private static boolean hasAnyValidTrapezoid(@NonNull BatteryChartViewModel viewModel) {
        // Sets the chart is clickable if there is at least one valid item in it.
        for (int trapezoidIndex = 0; trapezoidIndex < viewModel.size() - 1; trapezoidIndex++) {
            if (isTrapezoidValid(viewModel, trapezoidIndex)) {
                return true;
            }
        }
        return false;
    }

    private static String[] getPercentages() {
        return new String[] {
            formatPercentage(/* percentage= */ 100, /* round= */ true),
            formatPercentage(/* percentage= */ 50, /* round= */ true),
            formatPercentage(/* percentage= */ 0, /* round= */ true)
        };
    }

    private class BatteryChartAccessibilityNodeProvider extends AccessibilityNodeProvider {
        private static final int UNDEFINED = Integer.MIN_VALUE;

        private int mAccessibilityFocusNodeViewId = UNDEFINED;

        @Override
        public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
            if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
                final AccessibilityNodeInfo hostInfo =
                        new AccessibilityNodeInfo(BatteryChartView.this);
                for (int index = 0; index < mViewModel.size() - 1; index++) {
                    hostInfo.addChild(BatteryChartView.this, index);
                }
                return hostInfo;
            }
            final int index = virtualViewId;
            if (!isTrapezoidIndexValid(mViewModel, index)) {
                Log.w(TAG, "Invalid virtual view id:" + index);
                return null;
            }
            final AccessibilityNodeInfo childInfo =
                    new AccessibilityNodeInfo(BatteryChartView.this, index);
            final String slotTimeInfo = mViewModel.getContentDescription(index);
            final String batteryLevelInfo = mViewModel.getSlotBatteryLevelText(index);
            onInitializeAccessibilityNodeInfo(childInfo);
            childInfo.setClickable(isValidToDraw(mViewModel, index));
            childInfo.setText(slotTimeInfo);
            childInfo.setContentDescription(
                    mContext.getString(
                            R.string.battery_usage_status_time_info_and_battery_level,
                            mContext.getString(
                                    mViewModel.selectedIndex() == virtualViewId
                                            ? R.string.battery_chart_slot_status_selected
                                            : R.string.battery_chart_slot_status_unselected),
                            slotTimeInfo,
                            batteryLevelInfo));
            childInfo.setAccessibilityFocused(virtualViewId == mAccessibilityFocusNodeViewId);

            final Rect bounds = new Rect();
            getBoundsOnScreen(bounds, true);
            final int hostLeft = bounds.left;
            bounds.left = round(hostLeft + mTrapezoidSlots[index].mLeft);
            bounds.right = round(hostLeft + mTrapezoidSlots[index].mRight);
            childInfo.setBoundsInScreen(bounds);
            return childInfo;
        }

        @Override
        public boolean performAction(int virtualViewId, int action, @Nullable Bundle arguments) {
            if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
                return performAccessibilityAction(action, arguments);
            }
            switch (action) {
                case AccessibilityNodeInfo.ACTION_CLICK:
                    onTrapezoidClicked(BatteryChartView.this, virtualViewId);
                    return true;

                case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
                    mAccessibilityFocusNodeViewId = virtualViewId;
                    return sendAccessibilityEvent(
                            virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);

                case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
                    if (mAccessibilityFocusNodeViewId == virtualViewId) {
                        mAccessibilityFocusNodeViewId = UNDEFINED;
                    }
                    return sendAccessibilityEvent(
                            virtualViewId,
                            AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);

                default:
                    return performAccessibilityAction(action, arguments);
            }
        }
    }

    // A container class for each trapezoid left and right location.
    @VisibleForTesting
    static final class TrapezoidSlot {
        public float mLeft;
        public float mRight;

        @Override
        public String toString() {
            return String.format(Locale.US, "TrapezoidSlot[%f,%f]", mLeft, mRight);
        }
    }
}
