/*
 * Copyright (C) 2016 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.widget;

import static android.view.HapticFeedbackConstants.CLOCK_TICK;

import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.SeekBar;

import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.customview.widget.ExploreByTouchHelper;

import java.util.List;

/**
 * LabeledSeekBar represent a seek bar assigned with labeled, discrete values.
 * It pretends to be a group of radio button for AccessibilityServices, in order to adjust the
 * behavior of these services to keep the mental model of the visual discrete SeekBar.
 */
public class LabeledSeekBar extends SeekBar {

    private final ExploreByTouchHelper mAccessHelper;

    /** Seek bar change listener set via public method. */
    private OnSeekBarChangeListener mOnSeekBarChangeListener;

    /** Labels for discrete progress values. */
    private String[] mLabels;

    private int mLastProgress = -1;

    public LabeledSeekBar(Context context, AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.seekBarStyle);
    }

    public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        mAccessHelper = new LabeledSeekBarExploreByTouchHelper(this);
        ViewCompat.setAccessibilityDelegate(this, mAccessHelper);

        super.setOnSeekBarChangeListener(mProxySeekBarListener);
    }

    @Override
    public synchronized void setProgress(int progress) {
        // This method gets called from the constructor, so mAccessHelper may
        // not have been assigned yet.
        if (mAccessHelper != null) {
            mAccessHelper.invalidateRoot();
        }

        super.setProgress(progress);
    }

    public void setLabels(String[] labels) {
        mLabels = labels;
    }

    @Override
    public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) {
        // The callback set in the constructor will proxy calls to this
        // listener.
        mOnSeekBarChangeListener = l;
    }

    @Override
    protected boolean dispatchHoverEvent(MotionEvent event) {
        return mAccessHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
    }

    private void sendClickEventForAccessibility(int progress) {
        mAccessHelper.invalidateRoot();
        mAccessHelper.sendEventForVirtualView(progress, AccessibilityEvent.TYPE_VIEW_CLICKED);
    }

    private final OnSeekBarChangeListener mProxySeekBarListener = new OnSeekBarChangeListener() {
        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            if (mOnSeekBarChangeListener != null) {
                mOnSeekBarChangeListener.onStopTrackingTouch(seekBar);
            }
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            if (mOnSeekBarChangeListener != null) {
                mOnSeekBarChangeListener.onStartTrackingTouch(seekBar);
            }
        }

        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            if (mOnSeekBarChangeListener != null) {
                mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
                sendClickEventForAccessibility(progress);
            }
            if (progress != mLastProgress) {
                seekBar.performHapticFeedback(CLOCK_TICK);
                mLastProgress = progress;
            }
        }
    };

    private class LabeledSeekBarExploreByTouchHelper extends ExploreByTouchHelper {

        private boolean mIsLayoutRtl;

        public LabeledSeekBarExploreByTouchHelper(LabeledSeekBar forView) {
            super(forView);
            mIsLayoutRtl = forView.getResources().getConfiguration()
                    .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
        }

        @Override
        protected int getVirtualViewAt(float x, float y) {
            return getVirtualViewIdIndexFromX(x);
        }

        @Override
        protected void getVisibleVirtualViews(List<Integer> list) {
            for (int i = 0, c = LabeledSeekBar.this.getMax(); i <= c; ++i) {
                list.add(i);
            }
        }

        @Override
        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
                Bundle arguments) {
            if (virtualViewId == ExploreByTouchHelper.HOST_ID) {
                // Do nothing
                return false;
            }

            switch (action) {
                case AccessibilityNodeInfoCompat.ACTION_CLICK:
                    LabeledSeekBar.this.setProgress(virtualViewId);
                    sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED);
                    return true;
                default:
                    return false;
            }
        }

        @Override
        protected void onPopulateNodeForVirtualView(
                int virtualViewId, AccessibilityNodeInfoCompat node) {
            node.setClassName(RadioButton.class.getName());
            node.setBoundsInParent(getBoundsInParentFromVirtualViewId(virtualViewId));
            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
            node.setContentDescription(mLabels[virtualViewId]);
            node.setClickable(true);
            node.setCheckable(true);
            node.setChecked(virtualViewId == LabeledSeekBar.this.getProgress());
        }

        @Override
        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
            event.setClassName(RadioButton.class.getName());
            event.setContentDescription(mLabels[virtualViewId]);
            event.setChecked(virtualViewId == LabeledSeekBar.this.getProgress());
        }

        @Override
        protected void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) {
            node.setClassName(RadioGroup.class.getName());
        }

        @Override
        protected void onPopulateEventForHost(AccessibilityEvent event) {
            event.setClassName(RadioGroup.class.getName());
        }

        private int getHalfVirtualViewWidth() {
            final int width = LabeledSeekBar.this.getWidth();
            final int barWidth = width - LabeledSeekBar.this.getPaddingStart()
                    - LabeledSeekBar.this.getPaddingEnd();
            return Math.max(0, barWidth / (LabeledSeekBar.this.getMax() * 2));
        }

        private int getVirtualViewIdIndexFromX(float x) {
            int posBase = Math.max(0,
                    ((int) x - LabeledSeekBar.this.getPaddingStart()) / getHalfVirtualViewWidth());
            posBase = (posBase + 1) / 2;
            posBase = Math.min(posBase, LabeledSeekBar.this.getMax());
            return mIsLayoutRtl ? LabeledSeekBar.this.getMax() - posBase : posBase;
        }

        private Rect getBoundsInParentFromVirtualViewId(int virtualViewId) {
            final int updatedVirtualViewId = mIsLayoutRtl
                    ? LabeledSeekBar.this.getMax() - virtualViewId : virtualViewId;
            int left = (updatedVirtualViewId * 2 - 1) * getHalfVirtualViewWidth()
                    + LabeledSeekBar.this.getPaddingStart();
            int right = (updatedVirtualViewId * 2 + 1) * getHalfVirtualViewWidth()
                    + LabeledSeekBar.this.getPaddingStart();

            // Edge case
            left = updatedVirtualViewId == 0 ? 0 : left;
            right = updatedVirtualViewId == LabeledSeekBar.this.getMax()
                    ? LabeledSeekBar.this.getWidth() : right;

            final Rect r = new Rect();
            r.set(left, 0, right, LabeledSeekBar.this.getHeight());
            return r;
        }
    }
}
