package solar.blaz.date.week;

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.graphics.Paint.FontMetricsInt;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.text.TextDirectionHeuristicCompat;
import android.support.v4.text.TextDirectionHeuristicsCompat;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.SparseBooleanArray;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.DecelerateInterpolator;
import android.widget.OverScroller;

import org.threeten.bp.DayOfWeek;
import org.threeten.bp.LocalDate;
import org.threeten.bp.format.TextStyle;
import org.threeten.bp.temporal.ChronoUnit;
import org.threeten.bp.temporal.TemporalAdjusters;

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

/**
 * Created by Blaž Šolar on 24/01/14.
 */
public class WeekDatePicker extends View {

    public static final String TAG = "DatePicker";

    /**
     * The coefficient by which to adjust (divide) the max fling velocity.
     */
    private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 4;

    /**
     * The the duration for adjusting the selector wheel.
     */
    private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;

    /**
     * Determines speed during touch scrolling.
     */
    private VelocityTracker velocityTracker;

    /**
     * @see ViewConfiguration#getScaledMinimumFlingVelocity()
     */
    private int minimumFlingVelocity;

    /**
     * @see ViewConfiguration#getScaledMaximumFlingVelocity()
     */
    private int maximumFlingVelocity;

    private int touchSlop;

    private final LocalDate today;
    private LocalDate firstDay; // first day of week for current date
    private final DayOfWeek firstDayOfWeek;
    private final BoringLayout[] layouts = new BoringLayout[3 * 7]; // we are drawing 3 weeks at a time on screen
    private final BoringLayout[] dayLabelLayouts = new BoringLayout[7];

    @Nullable private final CharSequence[] labelNames;

    private final TextPaint dayTextPaint;
    private final TextPaint dayLabelTextPain;
    private final Paint selectedDayColor;

    private BoringLayout.Metrics dayMetrics;
    private BoringLayout.Metrics dayLabelMetrics;

    private ColorStateList dayTextColor;
    private ColorStateList dayLabelTextColor;

    private TextUtils.TruncateAt ellipsize;

    @Nullable private Drawable dayDrawable;
    @Nullable private Drawable indicatorDrawable;

    private int weekWidth;
    private int dayWidth;

    private float lastDownEventX;

    private OverScroller flingScrollerX;
    private OverScroller adjustScrollerX;

    private int previousScrollerX;

    private boolean scrollingX;
    private int scrollPositionStart;

    private OnWeekChanged onWeekChanged;
    private OnDateSelected onDateSelected;

    private final SparseBooleanArray dayIndicators = new SparseBooleanArray();

    private int selectedWeek;
    private int selectedDay;
    private int pressedDay = Integer.MIN_VALUE;

    private int dayDelta;

    private float dividerSize = 0;
    private float labelPadding = 0;

    @Nullable private Rect backgroundRect;
    @Nullable private Rect indicatorRect;

    private TextDirectionHeuristicCompat textDir;

    @Nullable private LocalDate fromDate;
    @Nullable private LocalDate toDate;

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

    public WeekDatePicker(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.weekDatePickerStyle);
    }

    public WeekDatePicker(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // create the selector wheel paint
        TextPaint paint = new TextPaint();
        paint.setAntiAlias(true);
        dayTextPaint = paint;

        dayLabelTextPain = new TextPaint();
        dayLabelTextPain.setAntiAlias(true);

        selectedDayColor = new Paint();
        selectedDayColor.setColor(Color.RED);
        selectedDayColor.setStyle(Style.FILL);

        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.WeekDatePicker,
                defStyle, 0
        );

        int ellipsize = 3; // END default value

        try {
            dayTextColor = a.getColorStateList(R.styleable.WeekDatePicker_android_textColor);
            if (dayTextColor == null) {
                dayTextColor = ColorStateList.valueOf(Color.BLACK);
            }

            dayLabelTextColor = a.getColorStateList(R.styleable.WeekDatePicker_wdp_labelTextColor);
            if (dayLabelTextColor == null) {
                dayLabelTextColor = ColorStateList.valueOf(Color.BLACK);
            }

            ellipsize = a.getInt(R.styleable.WeekDatePicker_android_ellipsize, ellipsize);
            dividerSize = a.getDimension(R.styleable.WeekDatePicker_wdp_dividerSize, dividerSize);

            float textSize = a.getDimension(R.styleable.WeekDatePicker_android_textSize, -1);
            if(textSize > -1) {
                setTextSize(textSize);
            }

            float labelTextSize = a.getDimension(R.styleable.WeekDatePicker_wdp_labelTextSize, -1);
            if (labelTextSize > -1) {
                setLabelTextSize(labelTextSize);
            }

            labelPadding = a.getDimension(R.styleable.WeekDatePicker_wdp_labelPadding, labelPadding);

            labelNames = a.getTextArray(R.styleable.WeekDatePicker_wdp_labelNames);

            dayDrawable = a.getDrawable(R.styleable.WeekDatePicker_wdp_dayBackground);
            indicatorDrawable = a.getDrawable(R.styleable.WeekDatePicker_wdp_indicatorDrawable);

            int dayOfWeek = a.getInt(R.styleable.WeekDatePicker_wdp_firstDayOfWeek, DayOfWeek.SUNDAY.getValue());
            firstDayOfWeek = DayOfWeek.of(dayOfWeek);

        } finally {
            a.recycle();
        }

        switch (ellipsize) {
            case 1:
                setEllipsize(TextUtils.TruncateAt.START);
                break;
            case 2:
                setEllipsize(TextUtils.TruncateAt.MIDDLE);
                break;
            case 3:
                setEllipsize(TextUtils.TruncateAt.END);
                break;
            case 4:
                setEllipsize(TextUtils.TruncateAt.MARQUEE);
                break;
        }

        buildFontMetrics();
        buildLabelFontMetrics();

//        setWillNotDraw(false);

        flingScrollerX = new OverScroller(context);
        adjustScrollerX = new OverScroller(context, new DecelerateInterpolator(2.5f));

        // initialize constants
        ViewConfiguration configuration = ViewConfiguration.get(context);
        touchSlop = configuration.getScaledTouchSlop();
        minimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        maximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
                / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;

        previousScrollerX = Integer.MIN_VALUE;

        calculateItemSize(getWidth(), getHeight());

//        mTouchHelper = new PickerTouchHelper(this);
//        ViewCompat.setAccessibilityDelegate(this, mTouchHelper);

        today = LocalDate.now();
        firstDay = getFirstDay(0);
        selectedDay = firstDay.until(today).getDays();

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        for (int i = 0; i < 2; i++) {

            if (i != 0 && width == getMeasuredWidth()) {
                break;
            }

            int height;
            if (heightMode == MeasureSpec.EXACTLY) {
                height = heightSize;
            } else {
                int labelTextHeight = Math.abs(dayLabelMetrics.ascent) + Math.abs(dayLabelMetrics.descent);
                labelTextHeight += getPaddingTop() + getPaddingBottom();

                int measuredWidth = getMeasuredWidth();
                if (measuredWidth == 0) {
                    measuredWidth = width;
                }

                int totalHeight = (int) (labelTextHeight + measuredWidth / 7 / 3 * 2 + labelPadding);

                if (heightMode == MeasureSpec.AT_MOST) {
                    height = Math.min(heightSize, totalHeight);
                } else {
                    height = totalHeight;
                }
            }

            setMeasuredDimension(width, height);
        }
    }

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

        float itemWithPadding = weekWidth + dividerSize;

        int saveCount = canvas.getSaveCount();
        canvas.save();

        int weekOffset = getSelectedWeek() - 1;
        float position = itemWithPadding * weekOffset;

        canvas.translate(position, getPaddingTop());

        for (int i = 0; i < 3; i++) {
            drawWeek(canvas, i * 7, weekOffset + i);
            canvas.translate(itemWithPadding, 0);
        }

        canvas.restoreToCount(saveCount);

    }

    private void drawWeek(Canvas canvas, int layoutIndex, int weekOffset) {

        int saveCount = canvas.save();

        int labelHeight = dayLabelLayouts[0].getHeight();
        float circleRadius = dayWidth / 3;
        int centerY = layouts[0].getHeight() / 2;
        float dateLineOffset = circleRadius - centerY;

        for (int i = 0; i < 7; i++) {

            int itemIndex = weekOffset * 7 + i;
            BoringLayout layout = layouts[layoutIndex + i];
            BoringLayout labelLayout = dayLabelLayouts[i];

            dayLabelTextPain.setColor(getTextColor(dayLabelTextColor, itemIndex));
            labelLayout.draw(canvas);

            dayTextPaint.setColor(getTextColor(dayTextColor, itemIndex));

            int count = canvas.save();
            canvas.translate(0, labelHeight + dateLineOffset + labelPadding);

            if (dayDrawable != null) {
                dayDrawable.setBounds(backgroundRect);
                dayDrawable.setState(getItemDrawableState(itemIndex));
                dayDrawable.draw(canvas);
            }

            if (indicatorDrawable != null && dayIndicators.get(itemIndex - dayDelta, false)) {
                indicatorDrawable.setBounds(indicatorRect);
                indicatorDrawable.setState(getItemDrawableState(itemIndex));
                indicatorDrawable.draw(canvas);
            }

            layout.draw(canvas);

            canvas.restoreToCount(count);

            canvas.translate(dayWidth, 0);
        }

        canvas.restoreToCount(saveCount);

    }

    private LocalDate getRelativeFirstDay(int weekOffset) {
        weekOffset = getSelectedWeek() + weekOffset;
        return getFirstDay(weekOffset);
    }

    private LocalDate getFirstDay(int weekOffset) {
        return getFirstDay().plusWeeks(weekOffset).with(TemporalAdjusters.previousOrSame(firstDayOfWeek));
    }

    @NonNull private LocalDate getFirstDay() {
        if (fromDate != null) {
            return fromDate;
        } else {
            return today;
        }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    @Override
    public void onRtlPropertiesChanged(int layoutDirection) {
        super.onRtlPropertiesChanged(layoutDirection);

        textDir = getTextDirectionHeuristic();
    }

    public void setLimits(@Nullable LocalDate from, @Nullable LocalDate to) {

        readjustIndexes(from);

        fromDate = from;
        toDate = to;

        firstDay = getFirstDay(0);

        invalidate();
    }

    private void readjustIndexes(@Nullable LocalDate newDate) {

        LocalDate from = newDate == null ? today : newDate;
        LocalDate to = fromDate == null ? today : fromDate;

        int delta = from.until(to).getDays();
        selectedDay += delta;
        dayDelta = delta;

    }

    private TextDirectionHeuristicCompat getTextDirectionHeuristic() {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {

            return TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR;

        } else {

            // Always need to resolve layout direction first
            final boolean defaultIsRtl = (getLayoutDirection() == LAYOUT_DIRECTION_RTL);

            switch (getTextDirection()) {
                default:
                case TEXT_DIRECTION_FIRST_STRONG:
                    return (defaultIsRtl ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL :
                            TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR);
                case TEXT_DIRECTION_ANY_RTL:
                    return TextDirectionHeuristicsCompat.ANYRTL_LTR;
                case TEXT_DIRECTION_LTR:
                    return TextDirectionHeuristicsCompat.LTR;
                case TEXT_DIRECTION_RTL:
                    return TextDirectionHeuristicsCompat.RTL;
                case TEXT_DIRECTION_LOCALE:
                    return TextDirectionHeuristicsCompat.LOCALE;
            }
        }
    }

    private void remakeLayout() {

        if (getWidth() > 0)  {

            LocalDate day = getRelativeFirstDay(-1);

            for (int i = 0; i < layouts.length; i++) {

                String dayText = String.valueOf(day.getDayOfMonth());
                if (layouts[i] == null) {
                    layouts[i] = BoringLayout.make(dayText, dayTextPaint, dayWidth,
                            Layout.Alignment.ALIGN_CENTER, 1f, 1f, dayMetrics, false, ellipsize,
                            dayWidth);
                } else {
                    layouts[i].replaceOrMake(dayText, dayTextPaint, dayWidth,
                            Layout.Alignment.ALIGN_CENTER, 1f, 1f, dayMetrics, false, ellipsize,
                            dayWidth);
                }

                day = day.plusDays(1);
            }

            DayOfWeek dayOfWeek = firstDayOfWeek; // first index is 1
            for (int i = 0; i < dayLabelLayouts.length; i++) {

                CharSequence name;
                if (labelNames == null) {
                    name = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault());
                } else {
                    int index = dayOfWeek.getValue() - 1;
                    name = labelNames[index];
                }


                if (dayLabelLayouts[i] == null) {
                    dayLabelLayouts[i] = BoringLayout.make(name, dayLabelTextPain, dayWidth,
                            Layout.Alignment.ALIGN_CENTER, 1f, 1f, dayLabelMetrics, false, ellipsize,
                            dayWidth);
                } else {
                    dayLabelLayouts[i].replaceOrMake(name, dayLabelTextPain, dayWidth,
                            Layout.Alignment.ALIGN_CENTER, 1f, 1f, dayLabelMetrics, false, ellipsize,
                            dayWidth);
                }

                dayOfWeek = dayOfWeek.plus(1);

            }

        }

    }

    /**
     * Calculates text color for specified item based on its position and state.
     *
     * @param item Index of item to get text color for
     * @return Item text color
     */
    private int getTextColor(ColorStateList color, int item) {

        List<Integer> states = new ArrayList<>();

        if (isItemEnabled(item)) {
            states.add(android.R.attr.state_enabled);
        }

        if (isItemPressed(item)) {
            states.add(android.R.attr.state_pressed);
        }

        if (isItemSelected(item)) {
            states.add(android.R.attr.state_selected);
        }

        int[] finalState = new int[states.size()];
        if (states.size() > 0) {
            for (int i = 0; i < states.size(); i++) {
                finalState[i] = states.get(i);
            }
        }

        return color.getColorForState(finalState, color.getDefaultColor());

    }

    private int[] getItemDrawableState(int item) {

        List<Integer> state = new ArrayList<>();
        if (isItemEnabled(item)) {
            state.add(android.R.attr.state_enabled);
        }

        if (isItemSelected(item)) {
            state.add(android.R.attr.state_selected);
        }

        if (isItemPressed(item)) {
            state.add(android.R.attr.state_pressed);
        }

        int[] intState = new int[state.size()];
        for (int i = 0; i < state.size(); i++) {
            intState[i] = state.get(i);
        }
        return intState;

    }

    private boolean isItemPressed(int item) {
        return item == pressedDay;
    }

    private boolean isItemSelected(int item) {
        return item == selectedDay;
    }

    private boolean isItemEnabled(int item) {
        LocalDate date = getDate(item);

        return (fromDate == null || fromDate.isEqual(date) || fromDate.isBefore(date))
                && (toDate == null || date.isEqual(toDate) || toDate.isAfter(date));
    }

    private LocalDate getDate(int day) {
        return firstDay.plusDays(day);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        calculateItemSize(w, h);
        calculateBackgroundRect();
        calculateIndicatorRect();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(!isEnabled()) {
            return false;
        }

        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        velocityTracker.addMovement(event);

        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_MOVE:

                float currentMoveX = event.getX();

                int deltaMoveX = (int) (lastDownEventX - currentMoveX);

                if(scrollingX || Math.abs(deltaMoveX) > touchSlop) {

                    if(!scrollingX) {
                        deltaMoveX = 0;
                        pressedDay = Integer.MIN_VALUE;
                        scrollingX = true;
                        getParent().requestDisallowInterceptTouchEvent(true);
                        scrollPositionStart = getScrollX();
                    }

                    scrollBy(deltaMoveX, 0);

                    lastDownEventX = currentMoveX;
                    invalidate();

                }

                break;
            case MotionEvent.ACTION_DOWN:

                if(!adjustScrollerX.isFinished()) {
                    adjustScrollerX.forceFinished(true);
                } else if(!flingScrollerX.isFinished()) {
                    flingScrollerX.forceFinished(true);
                } else {
                    scrollingX = false;
                }

                lastDownEventX = event.getX();

                if(!scrollingX) {
                    pressedDay = getDayPositionFromTouch(event.getX());
                }
                invalidate();

                break;
            case MotionEvent.ACTION_UP:

                VelocityTracker velocityTracker = this.velocityTracker;
                velocityTracker.computeCurrentVelocity(500, maximumFlingVelocity);
                int initialVelocityX = (int) velocityTracker.getXVelocity();

                if(scrollingX && Math.abs(initialVelocityX) > minimumFlingVelocity) {
                    flingX(initialVelocityX);
                } else {
                    float positionX = event.getX();
                    if(!scrollingX) {
                        int itemPos = getDayPositionFromTouch(positionX);
                        if (isItemEnabled(itemPos)) {
                            selectDay(itemPos);
                        }
                    } else if(scrollingX) {
                        finishScrolling();
                    }
                }

                this.velocityTracker.recycle();
                this.velocityTracker = null;

            case MotionEvent.ACTION_CANCEL:
                pressedDay = Integer.MIN_VALUE;
                invalidate();
                break;
        }

        return true;
    }

    public void selectDay(@NonNull LocalDate date) {
        int day = getDayForDate(date);
        selectDay(day);
    }

    @Override public void scrollTo(int x, int y) {
        if (fromDate != null && x < 0) {
            x = 0;
        }

        if (toDate != null) {
            float totalWidth = (weekWidth + dividerSize) * ChronoUnit.WEEKS.between(getFirstDay(), toDate);

            if (x > totalWidth) {
                x = (int) totalWidth;
            }
        }

        super.scrollTo(x, y);
    }

    private void selectDay(final int day) {

        if (selectedDay != day) {

            selectedDay = day;

            // post to the UI Thread to avoid potential interference with the OpenGL Thread
            if (onDateSelected != null) {
                final LocalDate date = getDate(day);
                post(new Runnable() {
                    @Override
                    public void run() {
                        onDateSelected.onDateSelected(date);
                    }
                });
            }

            invalidate();

        }

        int week = selectedDay / 7;
        if (selectedDay < 0 && selectedDay % 7 != 0) {
            week -= 1;
        }

        if (week != selectedWeek) {
            adjustToNearestWeekX(week);
        }

    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (!isEnabled()) {
            return super.onKeyDown(keyCode, event);
        }

        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
//                selectDay();
                return true;
            case KeyEvent.KEYCODE_DPAD_LEFT:
                smoothScrollBy(-1);
                return true;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                smoothScrollBy(1);
                return true;
            default:
                return super.onKeyDown(keyCode, event);
        }

    }

//    @Override
//    protected boolean dispatchHoverEvent(MotionEvent event) {
//
//        if (mTouchHelper.dispatchHoverEvent(event)) {
//            return true;
//        }
//
//        return super.dispatchHoverEvent(event);
//    }

    @Override
    public void computeScroll() {
        computeScrollX();
    }

    @Override
    public void getFocusedRect(Rect r) {
        super.getFocusedRect(r); // TODO this should only be current item
    }

    public void setOnWeekChangedListener(OnWeekChanged onWeekChanged) {
        this.onWeekChanged = onWeekChanged;
    }

    public void setOnDateSelectedListener(OnDateSelected onDateSelected) {
        this.onDateSelected = onDateSelected;
    }

    public int getSelectedWeek() {
        int x = getScrollX();
        return getWeekPositionFromCoordinates(x);
    }

    public void scrollToWeek(int index) {
        selectedWeek = index;
        scrollToItem(index);
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {

        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }

        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());

        scrollToWeek(ss.mSelItem);


    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        SavedState savedState = new SavedState(superState);
        savedState.mSelItem = selectedWeek;

        return savedState;

    }

    public TextUtils.TruncateAt getEllipsize() {
        return ellipsize;
    }

    public void setEllipsize(TextUtils.TruncateAt ellipsize) {
        if (this.ellipsize != ellipsize) {
            this.ellipsize = ellipsize;

            remakeLayout();
            invalidate();
        }
    }

    public void setDateIndicator(@NonNull LocalDate date, boolean enabled) {
        int itemIndex = getDayForDate(date);
        if (enabled) {
            dayIndicators.put(itemIndex, true);
        } else {
            dayIndicators.delete(itemIndex);
        }
    }

    private int getDayForDate(@NonNull LocalDate date) {
        return firstDay.until(date).getDays();
    }

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

    @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

        if (getWeekPositionFromCoordinates(l) != getWeekPositionFromCoordinates(oldl)) {
            remakeLayout();
        }
    }

    private int getDayPositionFromTouch(float x) {
        int weekPositionFromTouch = getWeekPositionFromTouch(x);
        int position = weekPositionFromTouch * 7 + getRelativeDayPositionFromTouch(x);
        if (weekPositionFromTouch < 0) {
            position += 6;
        }
        return position;
    }

    private int getRelativeDayPositionFromTouch(float x) {
        return getRelativeDayPositionFromCoordinates((int) (getScrollX() + x));
    }

    private int getWeekPositionFromTouch(float x) {
        return getWeekPositionFromCoordinates((int) (getScrollX() - (weekWidth + dividerSize) * .5f + x));
    }

    /**
     * Returns day of the week for passed coordinates.
     */
    private int getRelativeDayPositionFromCoordinates(int x) {
        float relativePosition = x % (weekWidth + dividerSize);
        return (int) (relativePosition / dayWidth);
    }

    private void computeScrollX() {
        OverScroller scroller = flingScrollerX;
        if(scroller.isFinished()) {
            scroller = adjustScrollerX;
            if(scroller.isFinished()) {
                return;
            }
        }

        if(scroller.computeScrollOffset()) {

            int currentScrollerX = scroller.getCurrX();
            if(previousScrollerX == Integer.MIN_VALUE) {
                previousScrollerX = scroller.getStartX();
            }

            scrollBy(currentScrollerX - previousScrollerX, 0);
            previousScrollerX = currentScrollerX;

            if(scroller.isFinished()) {
                onScrollerFinishedX(scroller);
            }

            postInvalidate();
        }
    }

    private void flingX(int velocityX) {

        int signum= Integer.signum(velocityX);

        int currentWeekPosition = getWeekPositionFromCoordinates(scrollPositionStart);
        float weekWidth = this.weekWidth + dividerSize;

        int finalPosition;
        switch (signum) {
            case -1:
                finalPosition = (int) weekWidth * (currentWeekPosition + 1);
                break;
            default:
            case 0:
                finalPosition = (int) weekWidth * (currentWeekPosition);
                break;
            case 1:
                finalPosition = (int) weekWidth * (currentWeekPosition - 1);
                break;
        }

        int dx = finalPosition - getScrollX();

        previousScrollerX = Integer.MIN_VALUE;
        flingScrollerX.startScroll(getScrollX(), getScrollY(), dx, 0);

        invalidate();
    }

    private void adjustToNearestWeekX() {

        int x = getScrollX();
        int week = Math.round(x / (weekWidth + dividerSize * 1f));
        adjustToNearestWeekX(week);

    }

    private void adjustToNearestWeekX(int week) {

        int x = getScrollX();

        if (selectedWeek != week) {
            selectedWeek = week;
            notifyWeekChange();
        }

        int weekPosition = (weekWidth + (int) dividerSize) * week;

        int deltaX = weekPosition - x;

        previousScrollerX = Integer.MIN_VALUE;
        adjustScrollerX.startScroll(x, 0, deltaX, 0, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
        invalidate();
    }

    private void calculateItemSize(int w, int h) {

        int items = 1;
        int totalPadding = ((int) dividerSize * (items - 1));
        weekWidth = (w - totalPadding) / items;
        dayWidth = weekWidth / 7;

        scrollToItem(selectedWeek);

        buildFontMetrics();
        buildLabelFontMetrics();
        remakeLayout();

    }

    private void calculateBackgroundRect() {

        if (dayDrawable != null) {
            float circleRadius = dayWidth / 3;
            int centerX = layouts[0].getWidth() / 2;
            int centerY = layouts[0].getHeight() / 2;

            backgroundRect = new Rect((int) (centerX - circleRadius), (int) (centerY - circleRadius),
                    (int) (centerX + circleRadius), (int) (centerY + circleRadius));
        } else {
            backgroundRect = null;
        }

    }

    private void calculateIndicatorRect() {

        if (indicatorDrawable != null) {

            float circleRadius = dayWidth / 3;
            int centerX = layouts[0].getWidth() / 2;
            int centerY = layouts[0].getHeight() / 2;

            int indicatorDotWidth = indicatorDrawable.getIntrinsicWidth();
            int indicatorDotHeight = indicatorDrawable.getIntrinsicHeight();
            indicatorRect = new Rect(centerX - indicatorDotWidth / 2,
                    (int) (centerY + circleRadius - indicatorDotHeight),
                    centerX + indicatorDotWidth / 2, (int) (centerY + circleRadius));

        } else {
            indicatorRect = null;
        }

    }

    private void onScrollerFinishedX(OverScroller scroller) {
        if(scroller == flingScrollerX) {
            finishScrolling();
        }
    }

    private void finishScrolling() {

        adjustToNearestWeekX();
        scrollingX = false;
    }

    private int getPositionOnScreen(float x) {
        return (int) (x / (weekWidth + dividerSize));
    }

    private void smoothScrollBy(int i) {
        int deltaMoveX = (weekWidth + (int) dividerSize) * i;

        previousScrollerX = Integer.MIN_VALUE;
        flingScrollerX.startScroll(getScrollX(), 0, deltaMoveX, 0);
        invalidate();
    }

    /**
     * Sets text size for items
     * @param size New item text size in px.
     */
    private void setTextSize(float size) {
        if(size != dayTextPaint.getTextSize()) {
            dayTextPaint.setTextSize(size);

            buildFontMetrics();
            requestLayout();
            invalidate();
        }
    }

    private void setLabelTextSize(float size) {
        if (size != dayLabelTextPain.getTextSize()) {
            dayLabelTextPain.setTextSize(size);

            buildLabelFontMetrics();
            remakeLayout();
            invalidate();
        }
    }

    /**
     * Calculates item from x coordinate position.
     * @param x Scroll position to calculate.
     * @return Selected item from scrolling position in {param x}
     */
    private int getWeekPositionFromCoordinates(int x) {
        return Math.round(x / (weekWidth + dividerSize));
    }

    /**
     * Scrolls to specified item.
     * @param index Index of an item to scroll to
     */
    private void scrollToItem(int index) {
        scrollTo((weekWidth + (int) dividerSize) * index, 0);
    }

    private void notifyWeekChange() {

        // post to the UI Thread to avoid potential interference with the OpenGL Thread
        if (onWeekChanged != null) {
            post(new Runnable() {
                @Override
                public void run() {
                    LocalDate firstDay = getFirstDay(getWeekPositionFromCoordinates(getScrollX()));
                    onWeekChanged.onItemSelected(firstDay);
                }
            });
        }

    }

    private void buildFontMetrics() {
        FontMetricsInt fontMetricsInt = dayTextPaint.getFontMetricsInt();
        dayMetrics = WeekDatePicker.toBoringFontMetrics(fontMetricsInt, dayMetrics);
        dayMetrics.width = weekWidth;
    }

    private void buildLabelFontMetrics() {
        FontMetricsInt fontMetricsInt = dayLabelTextPain.getFontMetricsInt();
        dayLabelMetrics = WeekDatePicker.toBoringFontMetrics(fontMetricsInt, dayLabelMetrics);
        dayLabelMetrics.width = weekWidth;
    }

    public interface OnWeekChanged {

        void onItemSelected(LocalDate firstDay);

    }

    public interface OnDateSelected {

        void onDateSelected(LocalDate date);

    }

    private static BoringLayout.Metrics toBoringFontMetrics(FontMetricsInt metrics,
            @Nullable BoringLayout.Metrics fontMetrics) {

        if (fontMetrics == null) {
            fontMetrics = new BoringLayout.Metrics();
        }

        fontMetrics.ascent = metrics.ascent;
        fontMetrics.bottom = metrics.bottom;
        fontMetrics.descent = metrics.descent;
        fontMetrics.leading = metrics.leading;
        fontMetrics.top = metrics.top;
        return fontMetrics;
    }

    public static class SavedState extends BaseSavedState {

        private int mSelItem;

        public SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            mSelItem = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);

            dest.writeInt(mSelItem);
        }

        @Override
        public String toString() {
            return  "HorizontalPicker.SavedState{"
                    + Integer.toHexString(System.identityHashCode(this))
                    + " selItem=" + mSelItem
                    + "}";
        }

        @SuppressWarnings("hiding")
        public static final Creator<SavedState> CREATOR
                = new Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

}