package org.solovyev.android.views;

import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.util.AttributeSet;
import android.view.animation.DecelerateInterpolator;
import android.widget.SeekBar;

import org.solovyev.android.Check;
import org.solovyev.android.calculator.R;

/**
 * SeekBar for discrete values with a label displayed underneath the active tick
 */
public class DiscreteSeekBar extends SeekBar {
    // Duration of how quick the SeekBar thumb should snap to its destination value
    private static final int THUMB_SNAP_DURATION_TIME = 100;
    private final Paint mPaint = new Paint();
    private ObjectAnimator mObjectAnimator;
    private OnChangeListener mOnChangeListener;
    private int mCurrentTick = 0;
    private CharSequence[] mTickLabels;
    private ColorStateList mLabelColor;

    public DiscreteSeekBar(Context context) {
        super(context);
        init(context, null, 0);
    }

    public DiscreteSeekBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0);
    }

    public DiscreteSeekBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs, defStyle);
    }

    private void init(Context context, AttributeSet attrs, int defStyle) {
        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.DiscreteSeekBar, defStyle, 0);
        mTickLabels = a.getTextArray(R.styleable.DiscreteSeekBar_values);
        final int labelsSize = a.getDimensionPixelSize(R.styleable.DiscreteSeekBar_labelsSize, 0);
        final ColorStateList labelsColor = a.getColorStateList(R.styleable.DiscreteSeekBar_labelsColor);
        a.recycle();

        Check.isNotNull(mTickLabels);
        Check.isTrue(mTickLabels.length > 0);
        Check.isTrue(labelsSize > 0);

        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        mPaint.setTextSize(labelsSize);

        if (labelsColor != null) {
            setLabelColor(labelsColor);
        } else {
            mPaint.setColor(Color.BLACK);
        }

        // Extend the bottom padding to include tick label height (including descent in order to not
        // clip glyphs that extends below the baseline).
        Paint.FontMetricsInt fi = mPaint.getFontMetricsInt();
        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
                getPaddingBottom() + labelsSize + fi.descent);

        super.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                    cancelAnimator();
                }
            }

            @TargetApi(Build.VERSION_CODES.HONEYCOMB)
            private void cancelAnimator() {
                if (mObjectAnimator != null) {
                    mObjectAnimator.cancel();
                }
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                mCurrentTick = getClosestTick(seekBar.getProgress());
                final int endProgress = getProgressForTick(mCurrentTick);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                    startAnimator(seekBar, endProgress);
                } else {
                    seekBar.setProgress(endProgress);
                }
                if (mOnChangeListener != null) {
                    mOnChangeListener.onValueChanged(mCurrentTick);
                }
            }

            @TargetApi(Build.VERSION_CODES.HONEYCOMB)
            private void startAnimator(SeekBar seekBar, int endProgress) {
                mObjectAnimator = ObjectAnimator.ofInt(
                        seekBar, "progress", seekBar.getProgress(), endProgress);
                mObjectAnimator.setInterpolator(new DecelerateInterpolator());
                mObjectAnimator.setDuration(THUMB_SNAP_DURATION_TIME);
                mObjectAnimator.start();
            }
        });
    }

    private int getClosestTick(int progress) {
        float normalizedValue = (float) progress / getMax();
        return Math.round(normalizedValue * getMaxTick());
    }

    private int getProgressForTick(int tick) {
        return (getMax() / getMaxTick()) * tick;
    }

    @Override
    public void setOnSeekBarChangeListener(OnSeekBarChangeListener seekBarChangeListener) {
        // It doesn't make sense to expose the interface for listening to intermediate changes.
        Check.isTrue(false);
    }

    /**
     * Get the largest tick value the SeekBar can represent
     *
     * @return maximum tick value
     */
    public int getMaxTick() {
        return mTickLabels.length - 1;
    }

    /**
     * Set listener for observing value changes
     *
     * @param onChangeListener listener that should receive updates
     */
    public void setOnChangeListener(OnChangeListener onChangeListener) {
        mOnChangeListener = onChangeListener;
    }

    /**
     * Set tick value
     *
     * @param tick tick value in range [0, maxTick]
     */
    public void setCurrentTick(int tick) {
        Check.isTrue(tick >= 0);
        Check.isTrue(tick <= getMaxTick());
        mCurrentTick = tick;
        setProgress(getProgressForTick(mCurrentTick));
    }

    public int getCurrentTick() {
        return mCurrentTick;
    }

    public void setLabelColor(int color) {
        mLabelColor = ColorStateList.valueOf(color);
        updateLabelColor();
    }

    public void setLabelColor(ColorStateList colors) {
        mLabelColor = colors;
        updateLabelColor();
    }

    private void updateLabelColor() {
        int color = mLabelColor.getColorForState(getDrawableState(), Color.BLACK);
        if (color != mPaint.getColor()) {
            mPaint.setColor(color);
            invalidate();
        }
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        updateLabelColor();
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        final float sliderWidth = getWidth() - getPaddingRight() - getPaddingLeft();
        final float sliderStepSize = sliderWidth / getMaxTick();
        int closestTick = getClosestTick(getProgress());
        String text = mTickLabels[closestTick].toString();
        final float startOffset = getPaddingLeft();
        final float tickLabelWidth = mPaint.measureText(text);
        final float tickPos = sliderStepSize * closestTick;
        final float labelOffset;
        // First step description text should be anchored with its left edge just
        // below the slider start tick. The last step description should be anchored
        // to the right just under the end tick. Tick labels in between are centered below
        // each tick.
        if (closestTick == 0) {
            labelOffset = startOffset;
        } else if (closestTick == getMaxTick()) {
            labelOffset = startOffset + sliderWidth - tickLabelWidth;
        } else {
            labelOffset = startOffset + tickPos - tickLabelWidth / 2;
        }
        // Text position is drawn from bottom left, with bottom at the font baseline. We need to
        // offset by the descent to cover e.g 'g' that extends below the baseline.
        final Paint.FontMetricsInt m = mPaint.getFontMetricsInt();
        final int lowestPosForFullGlyphCoverage = getHeight() - m.descent;
        canvas.drawText(text, labelOffset, lowestPosForFullGlyphCoverage, mPaint);
    }

    /**
     * Listener for observing tick changes
     */
    public interface OnChangeListener {
        void onValueChanged(int selectedTick);
    }
}
