package com.ptrstovka.calendarview2;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.ArrayRes;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.ptrstovka.calendarview2.format.ArrayWeekDayFormatter;
import com.ptrstovka.calendarview2.format.DateFormatTitleFormatter;
import com.ptrstovka.calendarview2.format.DayFormatter;
import com.ptrstovka.calendarview2.format.MonthArrayTitleFormatter;
import com.ptrstovka.calendarview2.format.TitleFormatter;
import com.ptrstovka.calendarview2.format.WeekDayFormatter;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;

/**
 * <p>
 * This class is a calendar widget for displaying and selecting dates.
 * The range of dates supported by this calendar is configurable.
 * A user can selectRanges a date by taping on it and can page the calendar to a desired date.
 * </p>
 * <p>
 * By default, the range of dates shown is from 200 years in the past to 200 years in the future.
 * This can be extended or shortened by configuring the minimum and maximum dates.
 * </p>
 * <p>
 * When selecting a date out of range, or when the range changes so the selection becomes outside,
 * The date closest to the previous selection will become selected. This will also trigger the
 * {@linkplain OnDateSelectedListener}
 * </p>
 * <p>
 * <strong>Note:</strong> if this view's size isn't divisible by 7,
 * the contents will be centered inside such that the days in the calendar are equally square.
 * For example, 600px isn't divisible by 7, so a tile size of 85 is choosen, making the calendar
 * 595px wide. The extra 5px are distributed left and right to get to 600px.
 * </p>
 */
public class CalendarView2 extends ViewGroup {

    public static final int INVALID_TILE_DIMENSION = -10;

    /**
     * {@linkplain IntDef} annotation for selection mode.
     *
     * @see #setSelectionMode(int)
     * @see #getSelectionMode()
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({SELECTION_MODE_NONE, SELECTION_MODE_SINGLE, SELECTION_MODE_MULTIPLE, SELECTION_MODE_RANGE})
    public @interface SelectionMode {
    }

    /**
     * Selection mode that disallows all selection.
     * When changing to this mode, current selection will be cleared.
     */
    public static final int SELECTION_MODE_NONE = 0;

    /**
     * Selection mode that allows one selected date at one time. This is the default mode.
     * When switching from {@linkplain #SELECTION_MODE_MULTIPLE}, this will selectRanges the same date
     * as from {@linkplain #getSelectedDate()}, which should be the last selected date
     */
    public static final int SELECTION_MODE_SINGLE = 1;

    /**
     * Selection mode which allows more than one selected date at one time.
     */
    public static final int SELECTION_MODE_MULTIPLE = 2;

    /**
     * Selection mode which allows selection of a range between two dates
     */
    public static final int SELECTION_MODE_RANGE = 3;

    /**
     * {@linkplain IntDef} annotation for showOtherDates.
     *
     * @see #setShowOtherDates(int)
     * @see #getShowOtherDates()
     */
    @SuppressLint("UniqueConstants")
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(flag = true, value = {
            SHOW_NONE, SHOW_ALL, SHOW_DEFAULTS,
            SHOW_OUT_OF_RANGE, SHOW_OTHER_MONTHS, SHOW_DECORATED_DISABLED
    })
    public @interface ShowOtherDates {
    }

    /**
     * Do not show any non-enabled dates
     */
    public static final int SHOW_NONE = 0;

    /**
     * Show dates from the proceeding and successive months, in a disabled state.
     * This flag also enables the {@link #SHOW_OUT_OF_RANGE} flag to prevent odd blank areas.
     */
    public static final int SHOW_OTHER_MONTHS = 1;

    /**
     * Show dates that are outside of the min-max range.
     * This will only show days from the current month unless {@link #SHOW_OTHER_MONTHS} is enabled.
     */
    public static final int SHOW_OUT_OF_RANGE = 2;

    /**
     * Show days that are individually disabled with decorators.
     * This will only show dates in the current month and inside the minimum and maximum date range.
     */
    public static final int SHOW_DECORATED_DISABLED = 4;

    /**
     * The default flags for showing non-enabled dates. Currently only shows {@link #SHOW_DECORATED_DISABLED}
     */
    public static final int SHOW_DEFAULTS = SHOW_DECORATED_DISABLED;

    /**
     * Show all the days
     */
    public static final int SHOW_ALL = SHOW_OTHER_MONTHS | SHOW_OUT_OF_RANGE | SHOW_DECORATED_DISABLED;

    /**
     * Use this orientation to animate the title vertically
     */
    public static final int VERTICAL = 0;

    /**
     * Use this orientation to animate the title horizontally
     */
    public static final int HORIZONTAL = 1;

    /**
     * Default tile size in DIPs. This is used in cases where there is no tile size specificed and the view is set to {@linkplain ViewGroup.LayoutParams#WRAP_CONTENT WRAP_CONTENT}
     */
    public static final int DEFAULT_TILE_SIZE_DP = 44;
    private static final int DEFAULT_DAYS_IN_WEEK = 7;
    private static final int DEFAULT_MAX_WEEKS = 6;
    private static final int DAY_NAMES_ROW = 1;

    private static final TitleFormatter DEFAULT_TITLE_FORMATTER = new DateFormatTitleFormatter();
    private final TitleChanger titleChanger;

    private final TextView title;
    private final DirectionButton buttonPast;
    private final DirectionButton buttonFuture;
    private final CalendarPager pager;
    private CalendarPagerAdapter<?> adapter;
    private CalendarDay currentMonth;
    private LinearLayout topbar;
    private CalendarMode calendarMode;
    /**
     * Used for the dynamic calendar height.
     */
    private boolean mDynamicHeightEnabled;

    private final ArrayList<DayViewDecorator> dayViewDecorators = new ArrayList<>();

    private final OnClickListener onClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (v == buttonFuture) {
                pager.setCurrentItem(pager.getCurrentItem() + 1, true);
            } else if (v == buttonPast) {
                pager.setCurrentItem(pager.getCurrentItem() - 1, true);
            }
        }
    };

    private final ViewPager.OnPageChangeListener pageChangeListener = new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageSelected(int position) {
            titleChanger.setPreviousMonth(currentMonth);
            currentMonth = adapter.getItem(position);
            updateUi();

            dispatchOnMonthChanged(currentMonth);
        }

        @Override
        public void onPageScrollStateChanged(int state) {
        }

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        }
    };

    private CalendarDay minDate = null;
    private CalendarDay maxDate = null;

    private OnDateSelectedListener listener;
    private OnMonthChangedListener monthListener;
    private OnRangeSelectedListener rangeListener;

    CharSequence calendarContentDescription;
    private int accentColor = 0;
    private int arrowColor = Color.BLACK;
    private Drawable leftArrowMask;
    private Drawable rightArrowMask;
    private int tileHeight = INVALID_TILE_DIMENSION;
    private int tileWidth = INVALID_TILE_DIMENSION;
    @SelectionMode
    private int selectionMode = SELECTION_MODE_SINGLE;
    private boolean allowClickDaysOutsideCurrentMonth = true;
    private int firstDayOfWeek;

    private int dayCirclePadding = 0;

    private State state;

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

    public CalendarView2(Context context, AttributeSet attrs) {
        super(context, attrs);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            //If we're on good Android versions, turn off clipping for cool effects
            setClipToPadding(false);
            setClipChildren(false);
        } else {
            //Old Android does not like _not_ clipping view pagers, we need to clip
            setClipChildren(true);
            setClipToPadding(true);
        }

        buttonPast = new DirectionButton(getContext());
        buttonPast.setContentDescription(getContext().getString(R.string.previous));
        title = new TextView(getContext());
        buttonFuture = new DirectionButton(getContext());
        buttonFuture.setContentDescription(getContext().getString(R.string.next));
        pager = new CalendarPager(getContext());

        buttonPast.setOnClickListener(onClickListener);
        buttonFuture.setOnClickListener(onClickListener);

        titleChanger = new TitleChanger(title);
        titleChanger.setTitleFormatter(DEFAULT_TITLE_FORMATTER);

        pager.setOnPageChangeListener(pageChangeListener);
        pager.setPageTransformer(false, new ViewPager.PageTransformer() {
            @Override
            public void transformPage(View page, float position) {
                position = (float) Math.sqrt(1 - Math.abs(position));
                page.setAlpha(position);
            }
        });

        TypedArray a = context.getTheme()
                .obtainStyledAttributes(attrs, R.styleable.CalendarView2, 0, 0);
        try {
            int calendarModeIndex = a.getInteger(
                    R.styleable.CalendarView2_mcv_calendarMode,
                    0
            );
            firstDayOfWeek = a.getInteger(
                    R.styleable.CalendarView2_mcv_firstDayOfWeek,
                    -1
            );

            titleChanger.setOrientation(
                    a.getInteger(R.styleable.CalendarView2_mcv_titleAnimationOrientation,
                            VERTICAL));

            if (firstDayOfWeek < 0) {
                //Allowing use of Calendar.getInstance() here as a performance optimization
                firstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
            }

            newState()
                    .setFirstDayOfWeek(firstDayOfWeek)
                    .setCalendarDisplayMode(CalendarMode.values()[calendarModeIndex])
                    .commit();

            @SuppressWarnings("ResourceType") final int tileSize = a.getLayoutDimension(R.styleable.CalendarView2_mcv_tileSize, INVALID_TILE_DIMENSION);
            if (tileSize > INVALID_TILE_DIMENSION) {
                setTileSize(tileSize);
            }

            @SuppressWarnings("ResourceType") final int tileWidth = a.getLayoutDimension(R.styleable.CalendarView2_mcv_tileWidth, INVALID_TILE_DIMENSION);
            if (tileWidth > INVALID_TILE_DIMENSION) {
                setTileWidth(tileWidth);
            }

            @SuppressWarnings("ResourceType") final int tileHeight = a.getLayoutDimension(R.styleable.CalendarView2_mcv_tileHeight, INVALID_TILE_DIMENSION);
            if (tileHeight > INVALID_TILE_DIMENSION) {
                setTileHeight(tileHeight);
            }

            setArrowColor(a.getColor(
                    R.styleable.CalendarView2_mcv_arrowColor,
                    Color.BLACK
            ));
            Drawable leftMask = a.getDrawable(
                    R.styleable.CalendarView2_mcv_leftArrowMask
            );
            if (leftMask == null) {
                leftMask = getResources().getDrawable(R.drawable.mcv_action_previous);
            }
            setLeftArrowMask(leftMask);
            Drawable rightMask = a.getDrawable(
                    R.styleable.CalendarView2_mcv_rightArrowMask
            );
            if (rightMask == null) {
                rightMask = getResources().getDrawable(R.drawable.mcv_action_next);
            }
            setRightArrowMask(rightMask);

            setSelectionColor(
                    a.getColor(
                            R.styleable.CalendarView2_mcv_selectionColor,
                            getThemeAccentColor(context)
                    )
            );

            CharSequence[] array = a.getTextArray(R.styleable.CalendarView2_mcv_weekDayLabels);
            if (array != null) {
                setWeekDayFormatter(new ArrayWeekDayFormatter(array));
            }

            array = a.getTextArray(R.styleable.CalendarView2_mcv_monthLabels);
            if (array != null) {
                setTitleFormatter(new MonthArrayTitleFormatter(array));
            }

            setHeaderTextAppearance(a.getResourceId(
                    R.styleable.CalendarView2_mcv_headerTextAppearance,
                    R.style.TextAppearance_MaterialCalendarWidget_Header
            ));
            setWeekDayTextAppearance(a.getResourceId(
                    R.styleable.CalendarView2_mcv_weekDayTextAppearance,
                    R.style.TextAppearance_MaterialCalendarWidget_WeekDay
            ));
            setDateTextAppearance(a.getResourceId(
                    R.styleable.CalendarView2_mcv_dateTextAppearance,
                    R.style.TextAppearance_MaterialCalendarWidget_Date
            ));
            //noinspection ResourceType
            setShowOtherDates(a.getInteger(
                    R.styleable.CalendarView2_mcv_showOtherDates,
                    SHOW_DEFAULTS
            ));

            setAllowClickDaysOutsideCurrentMonth(a.getBoolean(
                    R.styleable.CalendarView2_mcv_allowClickDaysOutsideCurrentMonth,
                    true
            ));

            setDayCirclePadding(a.getDimensionPixelOffset(
                    R.styleable.CalendarView2_mcv_circlePadding,
                    dayCirclePadding
            ));

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            a.recycle();
        }

        // Adapter is created while parsing the TypedArray attrs, so setup has to happen after
        adapter.setTitleFormatter(DEFAULT_TITLE_FORMATTER);
        setupChildren();

        currentMonth = CalendarDay.today();
        setCurrentDate(currentMonth);

        if (isInEditMode()) {
            removeView(pager);
            MonthView monthView = new MonthView(this, currentMonth, getFirstDayOfWeek());
            monthView.setSelectionColor(getSelectionColor());
            monthView.setDateTextAppearance(adapter.getDateTextAppearance());
            monthView.setWeekDayTextAppearance(adapter.getWeekDayTextAppearance());
            monthView.setShowOtherDates(getShowOtherDates());
            addView(monthView, new LayoutParams(calendarMode.visibleWeeksCount + DAY_NAMES_ROW));
        }
    }

    private void setupChildren() {
        topbar = new LinearLayout(getContext());
        topbar.setOrientation(LinearLayout.HORIZONTAL);
        topbar.setClipChildren(false);
        topbar.setClipToPadding(false);
        addView(topbar, new LayoutParams(1));

        buttonPast.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
        topbar.addView(buttonPast, new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1));

        title.setGravity(Gravity.CENTER);
        topbar.addView(title, new LinearLayout.LayoutParams(
                0, LayoutParams.MATCH_PARENT, DEFAULT_DAYS_IN_WEEK - 2
        ));

        buttonFuture.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
        topbar.addView(buttonFuture, new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1));

        pager.setId(R.id.mcv_pager);
        pager.setOffscreenPageLimit(1);
        addView(pager, new LayoutParams(calendarMode.visibleWeeksCount + DAY_NAMES_ROW));
    }

    private void updateUi() {
        titleChanger.change(currentMonth);
        buttonPast.setEnabled(canGoBack());
        buttonFuture.setEnabled(canGoForward());
    }

    /**
     * Change the selection mode of the calendar. The default mode is {@linkplain #SELECTION_MODE_SINGLE}
     *
     * @param mode the selection mode to change to. This must be one of
     *             {@linkplain #SELECTION_MODE_NONE}, {@linkplain #SELECTION_MODE_SINGLE},
     *             {@linkplain #SELECTION_MODE_RANGE} or {@linkplain #SELECTION_MODE_MULTIPLE}.
     *             Unknown values will act as {@linkplain #SELECTION_MODE_SINGLE}
     * @see #getSelectionMode()
     * @see #SELECTION_MODE_NONE
     * @see #SELECTION_MODE_SINGLE
     * @see #SELECTION_MODE_MULTIPLE
     * @see #SELECTION_MODE_RANGE
     */
    public void setSelectionMode(final @SelectionMode int mode) {
        final @SelectionMode int oldMode = this.selectionMode;
        this.selectionMode = mode;
        switch (mode) {
            case SELECTION_MODE_RANGE:
                clearSelection();
                break;
            case SELECTION_MODE_MULTIPLE:
                break;
            case SELECTION_MODE_SINGLE:
                if (oldMode == SELECTION_MODE_MULTIPLE || oldMode == SELECTION_MODE_RANGE) {
                    //We should only have one selection now, so we should pick one
                    List<CalendarDay> dates = getSelectedDates();
                    if (!dates.isEmpty()) {
                        setSelectedDate(getSelectedDate());
                    }
                }
                break;
            default:
            case SELECTION_MODE_NONE:
                this.selectionMode = SELECTION_MODE_NONE;
                if (oldMode != SELECTION_MODE_NONE) {
                    //No selection! Clear out!
                    clearSelection();
                }
                break;
        }

        adapter.setSelectionEnabled(selectionMode != SELECTION_MODE_NONE);
    }

    /**
     * Go to previous month or week without using the button {@link #buttonPast}. Should only go to
     * previous if {@link #canGoBack()} is true, meaning it's possible to go to the previous month
     * or week.
     */
    public void goToPrevious() {
        if (canGoBack()) {
            pager.setCurrentItem(pager.getCurrentItem() - 1, true);
        }
    }

    /**
     * Go to next month or week without using the button {@link #buttonFuture}. Should only go to
     * next if {@link #canGoForward()} is enabled, meaning it's possible to go to the next month or
     * week.
     */
    public void goToNext() {
        if (canGoForward()) {
            pager.setCurrentItem(pager.getCurrentItem() + 1, true);
        }
    }

    /**
     * Get the current selection mode. The default mode is {@linkplain #SELECTION_MODE_SINGLE}
     *
     * @return the current selection mode
     * @see #setSelectionMode(int)
     * @see #SELECTION_MODE_NONE
     * @see #SELECTION_MODE_SINGLE
     * @see #SELECTION_MODE_MULTIPLE
     * @see #SELECTION_MODE_RANGE
     */
    @SelectionMode
    public int getSelectionMode() {
        return selectionMode;
    }

    /**
     * Use {@link #getTileWidth()} or {@link #getTileHeight()} instead. This method is deprecated
     * and will just return the largest of the two sizes.
     *
     * @return tile height or width, whichever is larger
     */
    @Deprecated
    public int getTileSize() {
        return Math.max(tileHeight, tileWidth);
    }

    /**
     * Set the size of each tile that makes up the calendar.
     * Each day is 1 tile, so the widget is 7 tiles wide and 7 or 8 tiles tall
     * depending on the visibility of the {@link #topbar}.
     *
     * @param size the new size for each tile in pixels
     */
    public void setTileSize(int size) {
        this.tileWidth = size;
        this.tileHeight = size;
        requestLayout();
    }

    /**
     * @param tileSizeDp the new size for each tile in dips
     * @see #setTileSize(int)
     */
    public void setTileSizeDp(int tileSizeDp) {
        setTileSize(dpToPx(tileSizeDp));
    }

    /**
     * @return the height of tiles in pixels
     */
    public int getTileHeight() {
        return tileHeight;
    }

    /**
     * Set the height of each tile that makes up the calendar.
     *
     * @param height the new height for each tile in pixels
     */
    public void setTileHeight(int height) {
        this.tileHeight = height;
        requestLayout();
    }

    /**
     * @param tileHeightDp the new height for each tile in dips
     * @see #setTileHeight(int)
     */
    public void setTileHeightDp(int tileHeightDp) {
        setTileHeight(dpToPx(tileHeightDp));
    }

    /**
     * @return the width of tiles in pixels
     */
    public int getTileWidth() {
        return tileWidth;
    }

    /**
     * Set the width of each tile that makes up the calendar.
     *
     * @param width the new width for each tile in pixels
     */
    public void setTileWidth(int width) {
        this.tileWidth = width;
        requestLayout();
    }

    /**
     * @param tileWidthDp the new width for each tile in dips
     * @see #setTileWidth(int)
     */
    public void setTileWidthDp(int tileWidthDp) {
        setTileWidth(dpToPx(tileWidthDp));
    }

    private int dpToPx(int dp) {
        return (int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()
        );
    }

    /**
     * TODO should this be public?
     *
     * @return true if there is a future month that can be shown
     */
    public boolean canGoForward() {
        return pager.getCurrentItem() < (adapter.getCount() - 1);
    }

    /**
     * Pass all touch events to the pager so scrolling works on the edges of the calendar view.
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return pager.dispatchTouchEvent(event);
    }

    /**
     * TODO should this be public?
     *
     * @return true if there is a previous month that can be shown
     */
    public boolean canGoBack() {
        return pager.getCurrentItem() > 0;
    }

    /**
     * @return the color used for the selection
     */
    public int getSelectionColor() {
        return accentColor;
    }

    /**
     * @param color The selection color
     */
    public void setSelectionColor(int color) {
        if (color == 0) {
            if (!isInEditMode()) {
                return;
            } else {
                color = Color.GRAY;
            }
        }
        accentColor = color;
        adapter.setSelectionColor(color);
        invalidate();
    }

    public void setDayCirclePadding(int padding) {
        dayCirclePadding = padding;
        adapter.setDayCirclePadding(padding);
        invalidate();
    }

    /**
     * @return color used to draw arrows
     */
    public int getArrowColor() {
        return arrowColor;
    }

    /**
     * @param color the new color for the paging arrows
     */
    public void setArrowColor(int color) {
        if (color == 0) {
            return;
        }
        arrowColor = color;
        buttonPast.setColor(color);
        buttonFuture.setColor(color);
        invalidate();
    }

    /**
     * Set content description for button past
     *
     * @param description String to use as content description
     */
    public void setContentDescriptionArrowPast(final CharSequence description) {
        buttonPast.setContentDescription(description);
    }

    /**
     * Set content description for button future
     *
     * @param description String to use as content description
     */
    public void setContentDescriptionArrowFuture(final CharSequence description) {
        buttonFuture.setContentDescription(description);
    }

    /**
     * Set content description for calendar
     *
     * @param description String to use as content description
     */
    public void setContentDescriptionCalendar(final CharSequence description) {
        calendarContentDescription = description;
    }

    /**
     * Get content description for calendar
     *
     * @return calendar's content description
     */
    public CharSequence getCalendarContentDescription() {
        return calendarContentDescription != null
                ? calendarContentDescription
                : getContext().getString(R.string.calendar);
    }

    /**
     * @return icon used for the left arrow
     */
    public Drawable getLeftArrowMask() {
        return leftArrowMask;
    }

    /**
     * @param icon the new icon to use for the left paging arrow
     */
    public void setLeftArrowMask(Drawable icon) {
        leftArrowMask = icon;
        buttonPast.setImageDrawable(icon);
    }

    /**
     * @return icon used for the right arrow
     */
    public Drawable getRightArrowMask() {
        return rightArrowMask;
    }

    /**
     * @param icon the new icon to use for the right paging arrow
     */
    public void setRightArrowMask(Drawable icon) {
        rightArrowMask = icon;
        buttonFuture.setImageDrawable(icon);
    }

    /**
     * @param resourceId The text appearance resource id.
     */
    public void setHeaderTextAppearance(int resourceId) {
        title.setTextAppearance(getContext(), resourceId);
    }

    /**
     * @param resourceId The text appearance resource id.
     */
    public void setDateTextAppearance(int resourceId) {
        adapter.setDateTextAppearance(resourceId);
    }

    /**
     * @param resourceId The text appearance resource id.
     */
    public void setWeekDayTextAppearance(int resourceId) {
        adapter.setWeekDayTextAppearance(resourceId);
    }

    /**
     * @return the selected day, or null if no selection. If in multiple selection mode, this
     * will return the last selected date
     */
    public CalendarDay getSelectedDate() {
        List<CalendarDay> dates = adapter.getSelectedDates();
        if (dates.isEmpty()) {
            return null;
        } else {
            return dates.get(dates.size() - 1);
        }
    }

    /**
     * @return all of the currently selected dates
     */
    @NonNull
    public List<CalendarDay> getSelectedDates() {
        return adapter.getSelectedDates();
    }

    /**
     * Clear the currently selected date(s)
     */
    public void clearSelection() {
        List<CalendarDay> dates = getSelectedDates();
        adapter.clearSelections();
        for (CalendarDay day : dates) {
            dispatchOnDateSelected(day, false);
        }
    }

    /**
     * @param calendar a Calendar set to a day to selectRanges. Null to clear selection
     */
    public void setSelectedDate(@Nullable Calendar calendar) {
        setSelectedDate(CalendarDay.from(calendar));
    }

    /**
     * @param date a Date to set as selected. Null to clear selection
     */
    public void setSelectedDate(@Nullable Date date) {
        setSelectedDate(CalendarDay.from(date));
    }

    /**
     * @param date a Date to set as selected. Null to clear selection
     */
    public void setSelectedDate(@Nullable CalendarDay date) {
        clearSelection();
        if (date != null) {
            setDateSelected(date, true);
        }
    }

    /**
     * @param calendar a Calendar to change. Passing null does nothing
     * @param selected true if day should be selected, false to deselect
     */
    public void setDateSelected(@Nullable Calendar calendar, boolean selected) {
        setDateSelected(CalendarDay.from(calendar), selected);
    }

    /**
     * @param date     a Date to change. Passing null does nothing
     * @param selected true if day should be selected, false to deselect
     */
    public void setDateSelected(@Nullable Date date, boolean selected) {
        setDateSelected(CalendarDay.from(date), selected);
    }

    /**
     * @param day      a CalendarDay to change. Passing null does nothing
     * @param selected true if day should be selected, false to deselect
     */
    public void setDateSelected(@Nullable CalendarDay day, boolean selected) {
        if (day == null) {
            return;
        }
        adapter.setDateSelected(day, selected);
    }

    /**
     * @param calendar a Calendar set to a day to focus the calendar on. Null will do nothing
     */
    public void setCurrentDate(@Nullable Calendar calendar) {
        setCurrentDate(CalendarDay.from(calendar));
    }

    /**
     * @param date a Date to focus the calendar on. Null will do nothing
     */
    public void setCurrentDate(@Nullable Date date) {
        setCurrentDate(CalendarDay.from(date));
    }

    /**
     * @return The current month shown, will be set to first day of the month
     */
    public CalendarDay getCurrentDate() {
        return adapter.getItem(pager.getCurrentItem());
    }

    /**
     * @param day a CalendarDay to focus the calendar on. Null will do nothing
     */
    public void setCurrentDate(@Nullable CalendarDay day) {
        setCurrentDate(day, true);
    }

    /**
     * @param day             a CalendarDay to focus the calendar on. Null will do nothing
     * @param useSmoothScroll use smooth scroll when changing months.
     */
    public void setCurrentDate(@Nullable CalendarDay day, boolean useSmoothScroll) {
        if (day == null) {
            return;
        }
        int index = adapter.getIndexForDay(day);
        pager.setCurrentItem(index, useSmoothScroll);
        updateUi();
    }

    /**
     * @return the minimum selectable date for the calendar, if any
     */
    public CalendarDay getMinimumDate() {
        return minDate;
    }

    /**
     * @return the maximum selectable date for the calendar, if any
     */
    public CalendarDay getMaximumDate() {
        return maxDate;
    }

    /**
     * The default value is {@link #SHOW_DEFAULTS}, which currently is just {@link #SHOW_DECORATED_DISABLED}.
     * This means that the default visible days are of the current month, in the min-max range.
     *
     * @param showOtherDates flags for showing non-enabled dates
     * @see #SHOW_ALL
     * @see #SHOW_NONE
     * @see #SHOW_DEFAULTS
     * @see #SHOW_OTHER_MONTHS
     * @see #SHOW_OUT_OF_RANGE
     * @see #SHOW_DECORATED_DISABLED
     */
    public void setShowOtherDates(@ShowOtherDates int showOtherDates) {
        adapter.setShowOtherDates(showOtherDates);
    }

    /**
     * Allow the user to click on dates from other months that are not out of range. Go to next or
     * previous month if a day outside the current month is clicked. The day still need to be
     * enabled to be selected.
     * Default value is true. Should be used with {@link #SHOW_OTHER_MONTHS}.
     *
     * @param enabled True to allow the user to click on a day outside current month displayed
     */
    public void setAllowClickDaysOutsideCurrentMonth(final boolean enabled) {
        this.allowClickDaysOutsideCurrentMonth = enabled;
    }

    /**
     * Set a formatter for weekday labels.
     *
     * @param formatter the new formatter, null for default
     */
    public void setWeekDayFormatter(WeekDayFormatter formatter) {
        adapter.setWeekDayFormatter(formatter == null ? WeekDayFormatter.DEFAULT : formatter);
    }

    /**
     * Set a formatter for day labels.
     *
     * @param formatter the new formatter, null for default
     */
    public void setDayFormatter(DayFormatter formatter) {
        adapter.setDayFormatter(formatter == null ? DayFormatter.DEFAULT : formatter);
    }

    /**
     * Set a {@linkplain com.ptrstovka.calendarview2.format.WeekDayFormatter}
     * with the provided week day labels
     *
     * @param weekDayLabels Labels to use for the days of the week
     * @see com.ptrstovka.calendarview2.format.ArrayWeekDayFormatter
     * @see #setWeekDayFormatter(com.ptrstovka.calendarview2.format.WeekDayFormatter)
     */
    public void setWeekDayLabels(CharSequence[] weekDayLabels) {
        setWeekDayFormatter(new ArrayWeekDayFormatter(weekDayLabels));
    }

    /**
     * Set a {@linkplain com.ptrstovka.calendarview2.format.WeekDayFormatter}
     * with the provided week day labels
     *
     * @param arrayRes String array resource of week day labels
     * @see com.ptrstovka.calendarview2.format.ArrayWeekDayFormatter
     * @see #setWeekDayFormatter(com.ptrstovka.calendarview2.format.WeekDayFormatter)
     */
    public void setWeekDayLabels(@ArrayRes int arrayRes) {
        setWeekDayLabels(getResources().getTextArray(arrayRes));
    }

    /**
     * @return int of flags used for showing non-enabled dates
     * @see #SHOW_ALL
     * @see #SHOW_NONE
     * @see #SHOW_DEFAULTS
     * @see #SHOW_OTHER_MONTHS
     * @see #SHOW_OUT_OF_RANGE
     * @see #SHOW_DECORATED_DISABLED
     */
    @ShowOtherDates
    public int getShowOtherDates() {
        return adapter.getShowOtherDates();
    }

    /**
     * @return true if allow click on days outside current month displayed
     */
    public boolean allowClickDaysOutsideCurrentMonth() {
        return allowClickDaysOutsideCurrentMonth;
    }

    /**
     * Set a custom formatter for the month/year title
     *
     * @param titleFormatter new formatter to use, null to use default formatter
     */
    public void setTitleFormatter(TitleFormatter titleFormatter) {
        if (titleFormatter == null) {
            titleFormatter = DEFAULT_TITLE_FORMATTER;
        }
        titleChanger.setTitleFormatter(titleFormatter);
        adapter.setTitleFormatter(titleFormatter);
        updateUi();
    }

    /**
     * Set a {@linkplain com.ptrstovka.calendarview2.format.TitleFormatter}
     * using the provided month labels
     *
     * @param monthLabels month labels to use
     * @see com.ptrstovka.calendarview2.format.MonthArrayTitleFormatter
     * @see #setTitleFormatter(com.ptrstovka.calendarview2.format.TitleFormatter)
     */
    public void setTitleMonths(CharSequence[] monthLabels) {
        setTitleFormatter(new MonthArrayTitleFormatter(monthLabels));
    }

    /**
     * Set a {@linkplain com.ptrstovka.calendarview2.format.TitleFormatter}
     * using the provided month labels
     *
     * @param arrayRes String array resource of month labels to use
     * @see com.ptrstovka.calendarview2.format.MonthArrayTitleFormatter
     * @see #setTitleFormatter(com.ptrstovka.calendarview2.format.TitleFormatter)
     */
    public void setTitleMonths(@ArrayRes int arrayRes) {
        setTitleMonths(getResources().getTextArray(arrayRes));
    }

    /**
     * Change the title animation orientation to have a different look and feel.
     *
     * @param orientation {@link CalendarView2#VERTICAL} or {@link CalendarView2#HORIZONTAL}
     */
    public void setTitleAnimationOrientation(final int orientation) {
        titleChanger.setOrientation(orientation);
    }

    /**
     * Get the orientation of the animation of the title.
     *
     * @return Title animation orientation {@link CalendarView2#VERTICAL} or {@link CalendarView2#HORIZONTAL}
     */
    public int getTitleAnimationOrientation() {
        return titleChanger.getOrientation();
    }

    /**
     * Sets the visibility {@link #topbar}, which contains
     * the previous month button {@link #buttonPast}, next month button {@link #buttonFuture},
     * and the month title {@link #title}.
     *
     * @param visible Boolean indicating if the topbar is visible
     */
    public void setTopbarVisible(boolean visible) {
        topbar.setVisibility(visible ? View.VISIBLE : View.GONE);
        requestLayout();
    }

    /**
     * @return true if the topbar is visible
     */
    public boolean getTopbarVisible() {
        return topbar.getVisibility() == View.VISIBLE;
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        SavedState ss = new SavedState(super.onSaveInstanceState());
        ss.color = getSelectionColor();
        ss.dateTextAppearance = adapter.getDateTextAppearance();
        ss.weekDayTextAppearance = adapter.getWeekDayTextAppearance();
        ss.showOtherDates = getShowOtherDates();
        ss.allowClickDaysOutsideCurrentMonth = allowClickDaysOutsideCurrentMonth();
        ss.minDate = getMinimumDate();
        ss.maxDate = getMaximumDate();
        ss.selectedDates = getSelectedDates();
        ss.firstDayOfWeek = getFirstDayOfWeek();
        ss.orientation = getTitleAnimationOrientation();
        ss.selectionMode = getSelectionMode();
        ss.tileWidthPx = getTileWidth();
        ss.tileHeightPx = getTileHeight();
        ss.topbarVisible = getTopbarVisible();
        ss.calendarMode = calendarMode;
        ss.dynamicHeightEnabled = mDynamicHeightEnabled;
        ss.currentMonth = currentMonth;
        ss.cacheCurrentPosition = state.cacheCurrentPosition;
        return ss;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        newState()
                .setFirstDayOfWeek(ss.firstDayOfWeek)
                .setCalendarDisplayMode(ss.calendarMode)
                .setMinimumDate(ss.minDate)
                .setMaximumDate(ss.maxDate)
                .isCacheCalendarPositionEnabled(ss.cacheCurrentPosition)
                .commit();

        setSelectionColor(ss.color);
        setDateTextAppearance(ss.dateTextAppearance);
        setWeekDayTextAppearance(ss.weekDayTextAppearance);
        setShowOtherDates(ss.showOtherDates);
        setAllowClickDaysOutsideCurrentMonth(ss.allowClickDaysOutsideCurrentMonth);
        clearSelection();
        for (CalendarDay calendarDay : ss.selectedDates) {
            setDateSelected(calendarDay, true);
        }
        setTitleAnimationOrientation(ss.orientation);
        setTileWidth(ss.tileWidthPx);
        setTileHeight(ss.tileHeightPx);
        setTopbarVisible(ss.topbarVisible);
        setSelectionMode(ss.selectionMode);
        setDynamicHeightEnabled(ss.dynamicHeightEnabled);
        setCurrentDate(ss.currentMonth);
    }

    @Override
    protected void dispatchSaveInstanceState(@NonNull SparseArray<Parcelable> container) {
        dispatchFreezeSelfOnly(container);
    }

    @Override
    protected void dispatchRestoreInstanceState(@NonNull SparseArray<Parcelable> container) {
        dispatchThawSelfOnly(container);
    }

    private void setRangeDates(CalendarDay min, CalendarDay max) {
        CalendarDay c = currentMonth;
        adapter.setRangeDates(min, max);
        currentMonth = c;
        if (min != null) {
            currentMonth = min.isAfter(currentMonth) ? min : currentMonth;
        }
        int position = adapter.getIndexForDay(c);
        pager.setCurrentItem(position, false);
        updateUi();
    }

    public static class SavedState extends BaseSavedState {

        int color = 0;
        int dateTextAppearance = 0;
        int weekDayTextAppearance = 0;
        int showOtherDates = SHOW_DEFAULTS;
        boolean allowClickDaysOutsideCurrentMonth = true;
        CalendarDay minDate = null;
        CalendarDay maxDate = null;
        List<CalendarDay> selectedDates = new ArrayList<>();
        int firstDayOfWeek = Calendar.SUNDAY;
        int orientation = 0;
        int tileWidthPx = -1;
        int tileHeightPx = -1;
        boolean topbarVisible = true;
        int selectionMode = SELECTION_MODE_SINGLE;
        boolean dynamicHeightEnabled = false;
        CalendarMode calendarMode = CalendarMode.MONTHS;
        CalendarDay currentMonth = null;
        boolean cacheCurrentPosition;

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

        @Override
        public void writeToParcel(@NonNull Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(color);
            out.writeInt(dateTextAppearance);
            out.writeInt(weekDayTextAppearance);
            out.writeInt(showOtherDates);
            out.writeByte((byte) (allowClickDaysOutsideCurrentMonth ? 1 : 0));
            out.writeParcelable(minDate, 0);
            out.writeParcelable(maxDate, 0);
            out.writeTypedList(selectedDates);
            out.writeInt(firstDayOfWeek);
            out.writeInt(orientation);
            out.writeInt(tileWidthPx);
            out.writeInt(tileHeightPx);
            out.writeInt(topbarVisible ? 1 : 0);
            out.writeInt(selectionMode);
            out.writeInt(dynamicHeightEnabled ? 1 : 0);
            out.writeInt(calendarMode == CalendarMode.WEEKS ? 1 : 0);
            out.writeParcelable(currentMonth, 0);
            out.writeByte((byte) (cacheCurrentPosition ? 1 : 0));
        }

        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

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

        private SavedState(Parcel in) {
            super(in);
            color = in.readInt();
            dateTextAppearance = in.readInt();
            weekDayTextAppearance = in.readInt();
            showOtherDates = in.readInt();
            allowClickDaysOutsideCurrentMonth = in.readByte() != 0;
            ClassLoader loader = CalendarDay.class.getClassLoader();
            minDate = in.readParcelable(loader);
            maxDate = in.readParcelable(loader);
            in.readTypedList(selectedDates, CalendarDay.CREATOR);
            firstDayOfWeek = in.readInt();
            orientation = in.readInt();
            tileWidthPx = in.readInt();
            tileHeightPx = in.readInt();
            topbarVisible = in.readInt() == 1;
            selectionMode = in.readInt();
            dynamicHeightEnabled = in.readInt() == 1;
            calendarMode = in.readInt() == 1 ? CalendarMode.WEEKS : CalendarMode.MONTHS;
            currentMonth = in.readParcelable(loader);
            cacheCurrentPosition = in.readByte() != 0;
        }
    }

    private static int getThemeAccentColor(Context context) {
        int colorAttr;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            colorAttr = android.R.attr.colorAccent;
        } else {
            //Get colorAccent defined for AppCompat
            colorAttr = context.getResources().getIdentifier("colorAccent", "attr", context.getPackageName());
        }
        TypedValue outValue = new TypedValue();
        context.getTheme().resolveAttribute(colorAttr, outValue, true);
        return outValue.data;
    }

    /**
     * @return The first day of the week as a {@linkplain Calendar} day constant.
     */
    public int getFirstDayOfWeek() {
        return firstDayOfWeek;
    }

    /**
     * By default, the calendar will take up all the space needed to show any month (6 rows).
     * By enabling dynamic height, the view will change height dependant on the visible month.
     * <p>
     * This means months that only need 5 or 4 rows to show the entire month will only take up
     * that many rows, and will grow and shrink as necessary.
     *
     * @param useDynamicHeight true to have the view different heights based on the visible month
     */
    public void setDynamicHeightEnabled(boolean useDynamicHeight) {
        this.mDynamicHeightEnabled = useDynamicHeight;
    }

    /**
     * @return the dynamic height state - true if enabled.
     */
    public boolean isDynamicHeightEnabled() {
        return mDynamicHeightEnabled;
    }


    /**
     * Add a collection of day decorators
     *
     * @param decorators decorators to add
     */
    public void addDecorators(Collection<? extends DayViewDecorator> decorators) {
        if (decorators == null) {
            return;
        }

        dayViewDecorators.addAll(decorators);
        adapter.setDecorators(dayViewDecorators);
    }

    /**
     * Add several day decorators
     *
     * @param decorators decorators to add
     */
    public void addDecorators(DayViewDecorator... decorators) {
        addDecorators(Arrays.asList(decorators));
    }

    /**
     * Add a day decorator
     *
     * @param decorator decorator to add
     */
    public void addDecorator(DayViewDecorator decorator) {
        if (decorator == null) {
            return;
        }
        dayViewDecorators.add(decorator);
        adapter.setDecorators(dayViewDecorators);
    }

    /**
     * Remove all decorators
     */
    public void removeDecorators() {
        dayViewDecorators.clear();
        adapter.setDecorators(dayViewDecorators);
    }

    /**
     * Remove a specific decorator instance. Same rules as {@linkplain List#remove(Object)}
     *
     * @param decorator decorator to remove
     */
    public void removeDecorator(DayViewDecorator decorator) {
        dayViewDecorators.remove(decorator);
        adapter.setDecorators(dayViewDecorators);
    }

    /**
     * Invalidate decorators after one has changed internally. That is, if a decorator mutates, you
     * should call this method to update the widget.
     */
    public void invalidateDecorators() {
        adapter.invalidateDecorators();
    }

    public void invalidateDecorator(CalendarDay day) {
        adapter.invalidateDecorator(day);
    }

    /*
     * Listener/Callback Code
     */

    /**
     * Sets the listener to be notified upon selected date changes.
     *
     * @param listener thing to be notified
     */
    public void setOnDateChangedListener(OnDateSelectedListener listener) {
        this.listener = listener;
    }

    /**
     * Sets the listener to be notified upon month changes.
     *
     * @param listener thing to be notified
     */
    public void setOnMonthChangedListener(OnMonthChangedListener listener) {
        this.monthListener = listener;
    }

    /**
     * Sets the listener to be notified upon a range has been selected.
     *
     * @param listener thing to be notified
     */
    public void setOnRangeSelectedListener(OnRangeSelectedListener listener) {
        this.rangeListener = listener;
    }

    /**
     * Add listener to the title or null to remove it.
     *
     * @param listener Listener to be notified.
     */
    public void setOnTitleClickListener(final OnClickListener listener) {
        title.setOnClickListener(listener);
    }

    /**
     * Dispatch date change events to a listener, if set
     *
     * @param day      the day that was selected
     * @param selected true if the day is now currently selected, false otherwise
     */
    protected void dispatchOnDateSelected(final CalendarDay day, final boolean selected) {
        OnDateSelectedListener l = listener;
        if (l != null) {
            l.onDateSelected(CalendarView2.this, day, selected);
        }
    }

    protected void dispatchOnRangesSelected(Range... ranges) {
        for (Range range : ranges) {
            this.dispatchOnRangeSelected(range.from, range.to);
        }
    }

    /**
     * Dispatch a range of days to a listener, if set. First day must be before last Day.
     *
     * @param firstDay first day enclosing a range
     * @param lastDay  last day enclosing a range
     */
    protected void dispatchOnRangeSelected(final CalendarDay firstDay, final CalendarDay lastDay) {
        final OnRangeSelectedListener listener = rangeListener;
        final List<CalendarDay> days = new ArrayList<>();

        final Calendar counter = Calendar.getInstance();
        counter.setTime(firstDay.getDate());  //  start from the first day and increment

        final Calendar end = Calendar.getInstance();
        end.setTime(lastDay.getDate());  //  for comparison

        while (counter.before(end) || counter.equals(end)) {
            final CalendarDay current = CalendarDay.from(counter);
//            adapter.setDateSelected(current, true);
            days.add(current);
            counter.add(Calendar.DATE, 1);
        }

        Collections.sort(days, new CalendarDayComparator());
        adapter.setDateRangeSelected(days);

        if (listener != null) {
            listener.onRangeSelected(CalendarView2.this, days);
        }
    }

    /**
     * Dispatch date change events to a listener, if set
     *
     * @param day first day of the new month
     */
    protected void dispatchOnMonthChanged(final CalendarDay day) {
        OnMonthChangedListener l = monthListener;
        if (l != null) {
            l.onMonthChanged(CalendarView2.this, day);
        }
    }

    /**
     * Call by {@link CalendarPagerView} to indicate that a day was clicked and we should handle it.
     * This method will always process the click to the selected date.
     *
     * @param date        date of the day that was clicked
     * @param nowSelected true if the date is now selected, false otherwise
     */
    protected void onDateClicked(@NonNull CalendarDay date, boolean nowSelected) {
        switch (selectionMode) {
            case SELECTION_MODE_MULTIPLE: {
                adapter.setDateSelected(date, nowSelected);
                dispatchOnDateSelected(date, nowSelected);
            }
            break;
            case SELECTION_MODE_RANGE: {
                adapter.setDateSelected(date, nowSelected);
                if (adapter.getSelectedDates().size() > 2) {
                    adapter.clearSelections();
                    adapter.setDateSelected(date, nowSelected);  //  re-set because adapter has been cleared
                    dispatchOnDateSelected(date, nowSelected);
                } else if (adapter.getSelectedDates().size() == 2) {
                    final List<CalendarDay> dates = adapter.getSelectedDates();
                    if (dates.get(0).isAfter(dates.get(1))) {
                        dispatchOnRangeSelected(dates.get(1), dates.get(0));
                    } else {
                        dispatchOnRangeSelected(dates.get(0), dates.get(1));
                    }
                } else {
                    adapter.setDateSelected(date, nowSelected);
                    dispatchOnDateSelected(date, nowSelected);
                }
            }
            break;
            default:
            case SELECTION_MODE_SINGLE: {
                adapter.clearSelections();
                adapter.setDateSelected(date, true);
                dispatchOnDateSelected(date, true);
            }
            break;
        }
    }

    /**
     * Select a fresh range of date including first day and last day.
     *
     * @param firstDay first day of the range to selectRanges
     * @param lastDay  last day of the range to selectRanges
     */
    public void selectRange(final CalendarDay firstDay, final CalendarDay lastDay) {
        clearSelection();
        if (firstDay == null || lastDay == null) {
            return;
        } else if (firstDay.isAfter(lastDay)) {
            dispatchOnRangeSelected(lastDay, firstDay);
        } else {
            dispatchOnRangeSelected(firstDay, lastDay);
        }
    }

    private void select(boolean clearSelection, Range... ranges) {
        if (ranges.length == 0) {
            return;
        }

        if (ranges.length > 1) {
            if (this.selectionMode != SELECTION_MODE_NONE) {
                throw new IllegalArgumentException("Selection of multiple ranges is supported only with SELECTION_MODE_NONE.");
            }
        }

        if (clearSelection) {
            clearSelection();
        }

        selectRanges(ranges);
    }

    private void selectList(boolean clearSelection, List<Range> ranges) {
        if (ranges.size() == 0) {
            return;
        }

        if (ranges.size() > 1) {
            if (this.selectionMode != SELECTION_MODE_NONE) {
                throw new IllegalArgumentException("Selection of multiple ranges is supported only with SELECTION_MODE_NONE.");
            }
        }

        if (clearSelection) {
            clearSelection();
        }

        selectRanges(ranges);
    }

    public void select(Range... ranges) {
        select(true, ranges);
    }

    public void select(List<Range> ranges) {
        selectList(true, ranges);
    }

    public void add(Range... ranges) {
        select(false, ranges);
    }

    public void add(List<Range> ranges) {
        selectList(false, ranges);
    }

    /**
     * Select date range.
     * @param range
     */
    private void selectRanges(Range... range) {
        adapter.selectRanges(range);
    }

    private void selectRanges(List<Range> ranges){
        adapter.selectRanges(ranges);
    }

    /**
     * Call by {@link CalendarPagerView} to indicate that a day was clicked and we should handle it
     *
     * @param dayView
     */
    protected void onDateClicked(final DayView dayView) {
        final CalendarDay currentDate = getCurrentDate();
        final CalendarDay selectedDate = dayView.getDate();
        final int currentMonth = currentDate.getMonth();
        final int selectedMonth = selectedDate.getMonth();

        if (calendarMode == CalendarMode.MONTHS
                && allowClickDaysOutsideCurrentMonth
                && currentMonth != selectedMonth) {
            if (currentDate.isAfter(selectedDate)) {
                goToPrevious();
            } else if (currentDate.isBefore(selectedDate)) {
                goToNext();
            }
        }
        onDateClicked(dayView.getDate(), !dayView.isChecked());

    }

    /**
     * Called by the adapter for cases when changes in state result in dates being unselected
     *
     * @param date date that should be de-selected
     */
    protected void onDateUnselected(CalendarDay date) {
        dispatchOnDateSelected(date, false);
    }

    /*
     * Show Other Dates Utils
     */

    /**
     * @param showOtherDates int flag for show other dates
     * @return true if the other months flag is set
     */
    public static boolean showOtherMonths(@ShowOtherDates int showOtherDates) {
        return (showOtherDates & SHOW_OTHER_MONTHS) != 0;
    }

    /**
     * @param showOtherDates int flag for show other dates
     * @return true if the out of range flag is set
     */
    public static boolean showOutOfRange(@ShowOtherDates int showOtherDates) {
        return (showOtherDates & SHOW_OUT_OF_RANGE) != 0;
    }

    /**
     * @param showOtherDates int flag for show other dates
     * @return true if the decorated disabled flag is set
     */
    public static boolean showDecoratedDisabled(@ShowOtherDates int showOtherDates) {
        return (showOtherDates & SHOW_DECORATED_DISABLED) != 0;
    }

    /*
     * Custom ViewGroup Code
     */

    /**
     * {@inheritDoc}
     */
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(1);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        final int specWidthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int specWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int specHeightSize = MeasureSpec.getSize(heightMeasureSpec);
        final int specHeightMode = MeasureSpec.getMode(heightMeasureSpec);

        //We need to disregard padding for a while. This will be added back later
        final int desiredWidth = specWidthSize - getPaddingLeft() - getPaddingRight();
        final int desiredHeight = specHeightSize - getPaddingTop() - getPaddingBottom();

        final int weekCount = getWeekCountBasedOnMode();

        final int viewTileHeight = getTopbarVisible() ? (weekCount + 1) : weekCount;

        //Calculate independent tile sizes for later
        int desiredTileWidth = desiredWidth / DEFAULT_DAYS_IN_WEEK;
        int desiredTileHeight = desiredHeight / viewTileHeight;

        int measureTileSize = -1;
        int measureTileWidth = -1;
        int measureTileHeight = -1;

        if (this.tileWidth != INVALID_TILE_DIMENSION || this.tileHeight != INVALID_TILE_DIMENSION) {
            if (this.tileWidth > 0) {
                //We have a tileWidth set, we should use that
                measureTileWidth = this.tileWidth;
            } else {
                measureTileWidth = desiredTileWidth;
            }
            if (this.tileHeight > 0) {
                //We have a tileHeight set, we should use that
                measureTileHeight = this.tileHeight;
            } else {
                measureTileHeight = desiredTileHeight;
            }
        } else if (specWidthMode == MeasureSpec.EXACTLY || specWidthMode == MeasureSpec.AT_MOST) {
            if (specHeightMode == MeasureSpec.EXACTLY) {
                //Pick the smaller of the two explicit sizes
                measureTileSize = Math.min(desiredTileWidth, desiredTileHeight);
            } else {
                //Be the width size the user wants
                measureTileSize = desiredTileWidth;
            }
        } else if (specHeightMode == MeasureSpec.EXACTLY || specHeightMode == MeasureSpec.AT_MOST) {
            //Be the height size the user wants
            measureTileSize = desiredTileHeight;
        }

        if (measureTileSize > 0) {
            //Use measureTileSize if set
            measureTileHeight = measureTileSize;
            measureTileWidth = measureTileSize;
        } else if (measureTileSize <= 0) {
            if (measureTileWidth <= 0) {
                //Set width to default if no value were set
                measureTileWidth = dpToPx(DEFAULT_TILE_SIZE_DP);
            }
            if (measureTileHeight <= 0) {
                //Set height to default if no value were set
                measureTileHeight = dpToPx(DEFAULT_TILE_SIZE_DP);
            }
        }

        //Calculate our size based off our measured tile size
        int measuredWidth = measureTileWidth * DEFAULT_DAYS_IN_WEEK;
        int measuredHeight = measureTileHeight * viewTileHeight;

        //Put padding back in from when we took it away
        measuredWidth += getPaddingLeft() + getPaddingRight();
        measuredHeight += getPaddingTop() + getPaddingBottom();

        //Contract fulfilled, setting out measurements
        setMeasuredDimension(
                //We clamp inline because we want to use un-clamped versions on the children
                clampSize(measuredWidth, widthMeasureSpec),
                clampSize(measuredHeight, heightMeasureSpec)
        );

        int count = getChildCount();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);

            LayoutParams p = (LayoutParams) child.getLayoutParams();

            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                    DEFAULT_DAYS_IN_WEEK * measureTileWidth,
                    MeasureSpec.EXACTLY
            );

            int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    p.height * measureTileHeight,
                    MeasureSpec.EXACTLY
            );

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }

    private int getWeekCountBasedOnMode() {
        int weekCount = calendarMode.visibleWeeksCount;
        boolean isInMonthsMode = calendarMode.equals(CalendarMode.MONTHS);
        if (isInMonthsMode && mDynamicHeightEnabled && adapter != null && pager != null) {
            Calendar cal = (Calendar) adapter.getItem(pager.getCurrentItem()).getCalendar().clone();
            cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
            //noinspection ResourceType
            cal.setFirstDayOfWeek(getFirstDayOfWeek());
            weekCount = cal.get(Calendar.WEEK_OF_MONTH);
        }
        return weekCount + DAY_NAMES_ROW;
    }

    /**
     * Clamp the size to the measure spec.
     *
     * @param size Size we want to be
     * @param spec Measure spec to clamp against
     * @return the appropriate size to pass to {@linkplain View#setMeasuredDimension(int, int)}
     */
    private static int clampSize(int size, int spec) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        switch (specMode) {
            case MeasureSpec.EXACTLY: {
                return specSize;
            }
            case MeasureSpec.AT_MOST: {
                return Math.min(size, specSize);
            }
            case MeasureSpec.UNSPECIFIED:
            default: {
                return size;
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeft();
        final int parentWidth = right - left - parentLeft - getPaddingRight();

        int childTop = getPaddingTop();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }

            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int delta = (parentWidth - width) / 2;
            int childLeft = parentLeft + delta;

            child.layout(childLeft, childTop, childLeft + width, childTop + height);

            childTop += height;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(1);
    }

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

    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(1);
    }


    @Override
    public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(event);
        event.setClassName(CalendarView2.class.getName());
    }

    @Override
    public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        info.setClassName(CalendarView2.class.getName());
    }

    /**
     * Simple layout params for MaterialCalendarView. The only variation for layout is height.
     */
    protected static class LayoutParams extends MarginLayoutParams {

        /**
         * Create a layout that matches parent width, and is X number of tiles high
         *
         * @param tileHeight view height in number of tiles
         */
        public LayoutParams(int tileHeight) {
            super(MATCH_PARENT, tileHeight);
        }

    }

    /**
     * Enable or disable the ability to swipe between months.
     *
     * @param pagingEnabled pass false to disable paging, true to enable (default)
     */
    public void setPagingEnabled(boolean pagingEnabled) {
        pager.setPagingEnabled(pagingEnabled);
        updateUi();
    }

    /**
     * @return true if swiping months is enabled, false if disabled. Default is true.
     */
    public boolean isPagingEnabled() {
        return pager.isPagingEnabled();
    }

    /**
     * Preserve the current parameters of the Material Calendar View.
     */
    public State state() {
        return state;
    }

    /**
     * Initialize the parameters from scratch.
     */
    public StateBuilder newState() {
        return new StateBuilder();
    }

    public class State {
        private final CalendarMode calendarMode;
        private final int firstDayOfWeek;
        private final CalendarDay minDate;
        private final CalendarDay maxDate;
        private final boolean cacheCurrentPosition;

        private State(final StateBuilder builder) {
            calendarMode = builder.calendarMode;
            firstDayOfWeek = builder.firstDayOfWeek;
            minDate = builder.minDate;
            maxDate = builder.maxDate;
            cacheCurrentPosition = builder.cacheCurrentPosition;
        }

        /**
         * Modify parameters from current state.
         */
        public StateBuilder edit() {
            return new StateBuilder(this);
        }

    }

    public class StateBuilder {
        private CalendarMode calendarMode = CalendarMode.MONTHS;
        private int firstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
        private boolean cacheCurrentPosition = false;
        private CalendarDay minDate = null;
        private CalendarDay maxDate = null;

        public StateBuilder() {
        }

        private StateBuilder(final State state) {
            calendarMode = state.calendarMode;
            firstDayOfWeek = state.firstDayOfWeek;
            minDate = state.minDate;
            maxDate = state.maxDate;
            cacheCurrentPosition = state.cacheCurrentPosition;
        }

        /**
         * Sets the first day of the week.
         * <p>
         * Uses the java.util.Calendar day constants.
         *
         * @param day The first day of the week as a java.util.Calendar day constant.
         * @see java.util.Calendar
         */
        public StateBuilder setFirstDayOfWeek(int day) {
            this.firstDayOfWeek = day;
            return this;
        }

        /**
         * Set calendar display mode. The default mode is Months.
         * When switching between modes will selectRanges todays date, or the selected date,
         * if selection mode is single.
         *
         * @param mode - calendar mode
         */
        public StateBuilder setCalendarDisplayMode(CalendarMode mode) {
            this.calendarMode = mode;
            return this;
        }


        /**
         * @param calendar set the minimum selectable date, null for no minimum
         */
        public StateBuilder setMinimumDate(@Nullable Calendar calendar) {
            setMinimumDate(CalendarDay.from(calendar));
            return this;
        }

        /**
         * @param date set the minimum selectable date, null for no minimum
         */
        public StateBuilder setMinimumDate(@Nullable Date date) {
            setMinimumDate(CalendarDay.from(date));
            return this;
        }

        /**
         * @param calendar set the minimum selectable date, null for no minimum
         */
        public StateBuilder setMinimumDate(@Nullable CalendarDay calendar) {
            minDate = calendar;
            return this;
        }

        /**
         * @param calendar set the maximum selectable date, null for no maximum
         */
        public StateBuilder setMaximumDate(@Nullable Calendar calendar) {
            setMaximumDate(CalendarDay.from(calendar));
            return this;
        }

        /**
         * @param date set the maximum selectable date, null for no maximum
         */
        public StateBuilder setMaximumDate(@Nullable Date date) {
            setMaximumDate(CalendarDay.from(date));
            return this;
        }

        /**
         * @param calendar set the maximum selectable date, null for no maximum
         */
        public StateBuilder setMaximumDate(@Nullable CalendarDay calendar) {
            maxDate = calendar;
            return this;
        }

        /**
         * Use this method to enable saving the current position when switching
         * between week and month mode. By default, the calendar update to the latest selected date
         * or the current date. When set to true, the view will used the month that the calendar is
         * currently on.
         *
         * @param cacheCurrentPosition Set to true to cache the current position, false otherwise.
         */
        public StateBuilder isCacheCalendarPositionEnabled(final boolean cacheCurrentPosition) {
            this.cacheCurrentPosition = cacheCurrentPosition;
            return this;
        }

        public void commit() {
            CalendarView2.this.commit(new State(this));
        }
    }

    private void commit(State state) {
        // Use the calendarDayToShow to determine which date to focus on for the case of switching between month and week views
        CalendarDay calendarDayToShow = null;
        if (adapter != null && state.cacheCurrentPosition) {
            calendarDayToShow = adapter.getItem(pager.getCurrentItem());
            if (calendarMode != state.calendarMode) {
                CalendarDay currentlySelectedDate = getSelectedDate();
                if (calendarMode == CalendarMode.MONTHS && currentlySelectedDate != null) {
                    // Going from months to weeks
                    Calendar lastVisibleCalendar = calendarDayToShow.getCalendar();
                    lastVisibleCalendar.add(Calendar.MONTH, 1);
                    CalendarDay lastVisibleCalendarDay = CalendarDay.from(lastVisibleCalendar);
                    if (currentlySelectedDate.equals(calendarDayToShow) ||
                            (currentlySelectedDate.isAfter(calendarDayToShow) && currentlySelectedDate.isBefore(lastVisibleCalendarDay))) {
                        // Currently selected date is within view, so center on that
                        calendarDayToShow = currentlySelectedDate;
                    }
                } else if (calendarMode == CalendarMode.WEEKS) {
                    // Going from weeks to months
                    Calendar lastVisibleCalendar = calendarDayToShow.getCalendar();
                    lastVisibleCalendar.add(Calendar.DAY_OF_WEEK, 6);
                    CalendarDay lastVisibleCalendarDay = CalendarDay.from(lastVisibleCalendar);
                    if (currentlySelectedDate != null &&
                            (currentlySelectedDate.equals(calendarDayToShow) || currentlySelectedDate.equals(lastVisibleCalendarDay) ||
                                    (currentlySelectedDate.isAfter(calendarDayToShow) && currentlySelectedDate.isBefore(lastVisibleCalendarDay)))) {
                        // Currently selected date is within view, so center on that
                        calendarDayToShow = currentlySelectedDate;
                    } else {
                        calendarDayToShow = lastVisibleCalendarDay;
                    }
                }
            }
        }

        this.state = state;
        // Save states parameters
        calendarMode = state.calendarMode;
        firstDayOfWeek = state.firstDayOfWeek;
        minDate = state.minDate;
        maxDate = state.maxDate;

        // Recreate adapter
        final CalendarPagerAdapter<?> newAdapter;
        switch (calendarMode) {
            case MONTHS:
                newAdapter = new MonthPagerAdapter(this);
                break;
            case WEEKS:
                newAdapter = new WeekPagerAdapter(this);
                break;
            default:
                throw new IllegalArgumentException("Provided display mode which is not yet implemented");
        }
        if (adapter == null) {
            adapter = newAdapter;
        } else {
            adapter = adapter.migrateStateAndReturn(newAdapter);
        }
        pager.setAdapter(adapter);
        setRangeDates(minDate, maxDate);

        // Reset height params after mode change
        pager.setLayoutParams(new LayoutParams(calendarMode.visibleWeeksCount + DAY_NAMES_ROW));

        setCurrentDate(
                selectionMode == SELECTION_MODE_SINGLE && !adapter.getSelectedDates().isEmpty()
                        ? adapter.getSelectedDates().get(0)
                        : CalendarDay.today());

        if (calendarDayToShow != null) {
            pager.setCurrentItem(adapter.getIndexForDay(calendarDayToShow));
        }

        invalidateDecorators();
        updateUi();
    }
}