import * as utils from '../utils';
import * as type from '../utils/type';
import * as dateFns from 'date-fns';
import EventEmitter from '../utils/events';

import template from './templates/datepicker';
import templateDays from './templates/days';
import templateWeekdays from './templates/weekdays';
import templateMonths from './templates/months';
import templateYears from './templates/years';
import defaultOptions from './defaultOptions';

export default class datePicker extends EventEmitter {

    constructor(options = {}) {
        super();

        this.options = {
            ...defaultOptions,
            ...options,
        };

        this._clickEvents     = ['click', 'touch'];
        this._supportsPassive = utils.detectSupportsPassive();
        this._id              = utils.uuid('datePicker');
        this.node             = null;

        this.onPreviousDatePicker    = this.onPreviousDatePicker.bind(this);
        this.onNextDatePicker        = this.onNextDatePicker.bind(this);
        this.onSelectMonthDatePicker = this.onSelectMonthDatePicker.bind(this);
        this.onMonthClickDatePicker  = this.onMonthClickDatePicker.bind(this);
        this.onSelectYearDatePicker  = this.onSelectYearDatePicker.bind(this);
        this.onYearClickDatePicker   = this.onYearClickDatePicker.bind(this);
        this.onDateClickDatePicker   = this.onDateClickDatePicker.bind(this);

        this._init();
    }

    /****************************************************
     *                                                  *
     * GETTERS and SETTERS                              *
     *                                                  *
     ****************************************************/
    /**
     * Get id of current datePicker
     */
    get id() {
        return this._id;
    }

    set date(date) {
        if (type.isObject(date) && date.start && date.end) {
            this._date = date;
        }
        return this;
    }

    get date() {
        return (
            this._date || {
                start: undefined,
                end:   undefined,
            }
        );
    }

    // Set datePicker language
    set lang(lang) {
        try {
            this._locale = require(`date-fns/locale/${lang}/index.js`);
        } catch (e) {
            lang         = 'en-US';
            this._locale = require(`date-fns/locale/${lang}/index.js`);
        } finally {
            this._lang = lang;
            return this;
        }
    }

    // Get current datePicker language
    get lang() {
        return this._lang;
    }

    get locale() {
        return this._locale;
    }

    set start(date) {

        if (!date) {
            this._date.start = undefined;
            return this;
        }

        date = utils.newDate(date, this.format, 'yyyy-MM-dd');

        if (this._isValidDate(date, this.min, this.max)) {
            this._date.start = dateFns.startOfDay(date);
        }

        return this;

    }

    get start() {
        return this._date.start;
    }

    set end(date) {

        if (!date) {
            this._date.end = undefined;
            return this;
        }

        date = utils.newDate(date, this.format, 'yyyy-MM-dd');

        if (this._isValidDate(date, this.min, this.max)) {
            this._date.end = dateFns.endOfDay(date);
        }

        return this;

    }

    get end() {
        return this._date.end;
    }

    // Set min
    set min(date) {
        this._min = utils.newDate(date, this.format, 'yyyy-MM-dd');
        return this;
    }

    // Get min
    get min() {
        return this._min;
    }

    // Set max
    set max(date) {
        this._max = utils.newDate(date, this.format, 'yyyy-MM-dd');
        return this;
    }

    // Get max
    get max() {
        return this._max;
    }

    // Set date format (set to MM/dd/yyyy by default)
    set format(format) {
        this._format = format;
        return this;
    }

    // Get date format
    get format() {
        return this._format;
    }

    /****************************************************
     *                                                  *
     * EVENTS FUNCTIONS                                 *
     *                                                  *
     ****************************************************/
    onPreviousDatePicker(e) {

        if (!this._supportsPassive) {
            e.preventDefault();
        }
        e.stopPropagation();

        const prevMonth = dateFns.lastDayOfMonth(dateFns.subMonths(new Date(dateFns.getYear(this._visibleDate), dateFns.getMonth(this._visibleDate)), 1));
        const day       = Math.min(dateFns.getDaysInMonth(prevMonth), dateFns.getDate(this._visibleDate));

        this._setVisibleDate(dateFns.setDate(prevMonth, day));
        this.refresh();

    }

    onNextDatePicker(e) {

        if (!this._supportsPassive) {
            e.preventDefault();
        }
        e.stopPropagation();

        const nextMonth   = dateFns.addMonths(this._visibleDate, 1);
        const day         = Math.min(dateFns.getDaysInMonth(nextMonth), dateFns.getDate(this._visibleDate));

        this._setVisibleDate(dateFns.setDate(nextMonth, day));
        this.refresh();

    }

    onSelectMonthDatePicker(e) {

        if (!this._supportsPassive) {
            e.preventDefault();
        }
        e.stopPropagation();

        if (this.options.enableMonthSwitch) {
            this._ui.body.dates.classList.remove('is-active');
            this._ui.body.years.classList.remove('is-active');
            this._ui.body.months.classList.add('is-active');
            this._toggleNextButton(false);
            this._togglePreviousButton(false);
        }

    }

    onSelectYearDatePicker(e) {

        if (!this._supportsPassive) {
            e.preventDefault();
        }
        e.stopPropagation();

        if (this.options.enableYearSwitch) {
            this._ui.body.dates.classList.remove('is-active');
            this._ui.body.months.classList.remove('is-active');
            this._ui.body.years.classList.add('is-active');
            this._toggleNextButton(false);
            this._togglePreviousButton(false);

            const currentYear = this._ui.body.years.querySelector('.calendar-year.is-active');
            if (currentYear) {
                this._ui.body.years.scrollTop = currentYear.offsetTop - this._ui.body.years.offsetTop - this._ui.body.years.clientHeight / 2;
            }
        }

    }

    onMonthClickDatePicker(e) {

        if (!this._supportsPassive) {
            e.preventDefault();
        }
        e.stopPropagation();

        const newDate     = dateFns.setMonth(this._visibleDate, parseInt(e.currentTarget.dataset.month) - 1);
        this._visibleDate = this.min ? dateFns.max([newDate, this.min]) : newDate;
        this._visibleDate = this.max ? dateFns.min([this._visibleDate, this.max]) : this._visibleDate;

        this.refresh();

    }

    onYearClickDatePicker(e) {

        if (!this._supportsPassive) {
            e.preventDefault();
        }
        e.stopPropagation();

        const newDate     = dateFns.setYear(this._visibleDate, parseInt(e.currentTarget.dataset.year));
        this._visibleDate = this.min ? dateFns.max([newDate, this.min]) : newDate;
        this._visibleDate = this.max ? dateFns.min([this._visibleDate, this.max]) : this._visibleDate;

        this.refresh();

    }

    onDateClickDatePicker(e) {

        if (!this._supportsPassive) {
            e.preventDefault();
        }
        e.stopPropagation();

        if (!e.currentTarget.classList.contains('is-disabled')) {
            this._select(new Date(e.currentTarget.dataset.date));
            this.refresh();
        }

    }

    /****************************************************
     *                                                  *
     * PUBLIC FUNCTIONS                                 *
     *                                                  *
     ****************************************************/
    isRange() {
        return this.options.isRange;
    }

    isValid() {

        if (this.options.isRange) {
            return this._isValidDate(this.start) && this._isValidDate(this.end);
        }

        return this._isValidDate(this.start);

    }

    enableDate(date = undefined) {
        if (!Array.isArray(this.disabledDates)) return;
        const index = this.disabledDates.findIndex((disableDate) => dateFns.isEqual(disableDate, date));
        if (index > -1) {
            unset(this.disabledDates[index]);
        }
    }

    disableDate(date = undefined) {
        if (!Array.isArray(this.disabledDates)) return;
        const index = this.disabledDates.findIndex((disableDate) => dateFns.isEqual(disableDate, date));
        if (index === -1) {
            this.disabledDates.push(date);
        }
    }

    highlightDate(date = undefined) {
        if (!Array.isArray(this.highlightedDates)) return;
        const index = this.highlightedDates.findIndex((highlightDate) => dateFns.isEqual(highlightDate, date));
        if (index > -1) {
            unset(this.highlightedDates[index]);
        }
    }

    unhighlightDate(date = undefined) {
        if (!Array.isArray(this.highlightedDates)) return;
        const index = this.highlightedDates.findIndex((highlightDate) => dateFns.isEqual(highlightDate, date));
        if (index === -1) {
            this.highlightedDates.push(date);
        }
    }

    enableWeekDay(day) {
        const index = this.disabledWeekDays.findIndex((disabledWeekDay) => dateFns.isEqual(disabledWeekDay, day));
        if (index > -1) {
            unset(this.disabledWeekDays[index]);
        }
    }

    disableWeekDay(day) {
        const index = this.disabledWeekDays.findIndex((disabledWeekDay) => dateFns.isEqual(disabledWeekDay, day));
        if (index === -1) {
            this.disabledWeekDays.push(day);
        }
    }

    show() {

        if (this._open) return;

        this.refresh();
        this._ui.container.classList.add('is-active');

        this._open  = true;
        this._focus = true;

        this.emit('show', this);

    }

    hide() {

        if (!this._open) return;

        this._open  = false;
        this._focus = false;

        this._ui.container.classList.remove('is-active');

        this.emit('hide', this);

    }

    toggle() {
        if (!this._open) {
            this.show();
        } else {
            this.hide();
        }
    }

    /**
     * Get / Set datePicker value
     * @param {null|Date|Object|String} date optional if null then return the current date as String
     */
    value(value = null) {

        // Set
        if (value) {

            if (this.options.isRange) {

                if (type.isString(value)) {
                    value = value.split(' - ');
                }

                if (Array.isArray(value)) {
                    if (value.length)       this.start = utils.newDate(value[0], this.format, 'yyyy-MM-dd');
                    if (value.length === 2) this.end   = utils.newDate(value[1], this.format, 'yyyy-MM-dd');
                } else {
                    this._select(value, false);
                }

            } else {
                this._select(value, false);
            }

            this.refresh();

            return;

        }

        // Get
        let string = this._isValidDate(this.start) ? dateFns.format(this.start, this.format, { locale: this.locale }) : '';

        if (this.options.isRange && this._isValidDate(this.end)) {
            string += ` - ${dateFns.format(this.end, this.format, { locale: this.locale })}`;
        }

        return string;

    }

    /**
     * Refresh datepicker with new year/month days
     * @method _refreshdatepicker
     * @return {[type]}        [description]
     */
    refresh() {

        this._ui.body.dates.innerHTML = '';

        // the 12 months of the year
        const monthLabels = new Array(12).fill(dateFns.startOfYear(this._visibleDate)).map((d, i) => dateFns.addMonths(d, i));

        this._ui.body.months.innerHTML = '';
        this._ui.body.months.appendChild(
            document.createRange().createContextualFragment(
                templateMonths({
                    months: monthLabels,
                    format: this.options.formats.selectMonth,
                    locale: this.locale,
                })
            )
        );

        const months = this._ui.body.months.querySelectorAll('.datepicker-month') || [];
        months.forEach((month) => {
            this._clickEvents.forEach((clickEvent) => {
                month.addEventListener(clickEvent, this.onMonthClickDatePicker);
            });
            month.classList.remove('is-active');
            if (month.dataset.month === dateFns.format(this._visibleDate, 'MM', { locale: this.locale })) {
                month.classList.add('is-active');
            }
        });

        // years
        const yearLabels = new Array(this.options.displayYearsCount * 2).fill(dateFns.subYears(this._visibleDate, this.options.displayYearsCount)).map((d, i) =>
            dateFns.addYears(d, i)
        );
        this._ui.body.years.innerHTML = '';
        this._ui.body.years.appendChild(
            document.createRange().createContextualFragment(
                templateYears({
                    visibleDate: this._visibleDate,
                    years:       yearLabels,
                    format:      this.options.formats.selectYear,
                    locale:      this.locale,
                })
            )
        );
        const years = this._ui.body.years.querySelectorAll('.datepicker-year') || [];
        years.forEach((year) => {
            this._clickEvents.forEach((clickEvent) => {
                year.addEventListener(clickEvent, this.onYearClickDatePicker);
            });
            year.classList.remove('is-active');
            if (year.dataset.year === dateFns.format(this._visibleDate, 'yyyy', { locale: this.locale })) {
                year.classList.add('is-active');
            }
        });

        // the 7 days of the week (Sun-Sat)
        const weekdayLabels = new Array(7).fill(dateFns.startOfWeek(this._visibleDate, {weekStartsOn: this.options.weekStart})).map((d, i) =>
            dateFns.addDays(d, i)
        );
        this._ui.body.dates.appendChild(
            document.createRange().createContextualFragment(
                templateWeekdays({
                    days:   weekdayLabels,
                    format: this.options.formats.weekday,
                    locale: this.locale,
                })
            )
        );

        if (this.min && dateFns.differenceInCalendarMonths(this._visibleDate, this.min) === 0) {
            this._togglePreviousButton(false);
        } else {
            this._togglePreviousButton();
        }

        if (this.max && dateFns.differenceInCalendarMonths(this._visibleDate, this.max) === 0) {
            this._toggleNextButton(false);
        } else {
            this._toggleNextButton();
        }

        this._ui.navigation.month.innerHTML = dateFns.format(this._visibleDate, this.options.formats.navigationMonth, {locale: this.locale});
        this._ui.navigation.year.innerHTML  = dateFns.format(this._visibleDate, this.options.formats.navigationYear, {locale: this.locale});

        this._renderDays();

        this._ui.body.dates.classList.add('is-active');
        this._ui.body.months.classList.remove('is-active');
        this._ui.body.years.classList.remove('is-active');

        return this;

    }

    clear() {

        const today = new Date();

        this._date = {
            start: undefined,
            end:   undefined,
        };

        this._setVisibleDate(today);
        this.refresh();

    }

    render() {
        this.refresh();
        return this.node;
    }

    /****************************************************
     *                                                  *
     * PRIVATE FUNCTIONS                                *
     *                                                  *
     ****************************************************/
    _init() {

        const today = new Date();

        this._open  = false;
        this.lang   = this.options.lang;
        this.format = this.options.dateFormat || 'MM/dd/yyyy';

        this.disabledDates    = type.isFunction(this.options.disabledDates) ? this.options.disabledDates : [];
        this.disabledWeekDays = type.isString(this.options.disabledWeekDays) ? this.options.disabledWeekDays.split(',') : Array.isArray(this.options.disabledWeekDays) ? this.options.disabledWeekDays : [];
        this.highlightedDates = type.isFunction(this.options.highlightedDates) ? this.options.highlightedDates : [];

        if (Array.isArray(this.options.disabledDates)) {
            for (let i = 0; i < this.options.disabledDates.length; i++) {
                this.disabledDates.push(utils.newDate(this.options.disabledDates[i], this.format, 'yyyy-MM-dd'));
            }
        }

        if (Array.isArray(this.options.highlightedDates)) {
            for (let i = 0; i < this.options.highlightedDates.length; i++) {
                this.highlightedDates.push(utils.newDate(this.options.highlightedDates[i], this.format, 'yyyy-MM-dd'));
            }
        }

        this.min = this.options.minDate;
        this.max = this.options.maxDate;
        this._date = {
            start: utils.newDate(this.options.startDate, this.format, 'yyyy-MM-dd'),
            end:   this.options.isRange ? utils.newDate(this.options.endDate, this.format, 'yyyy-MM-dd') : undefined,
        };

        // set visible date on start or today
        this._visibleDate = this._isValidDate(this.start) ? this.start : today;

        // check if visible date is in the valid min/max range and adjust if necessary
        if (!this._isValidDate(this._visibleDate, this.min, this.max)) {
            this._visibleDate = this.min ? this.min : this.max;
        }

        this._build();
        this._bindEvents();

        this.emit('ready', this);

    }

    _build() {
        this.node = document.createRange().createContextualFragment(
            template({
                locale:      this.locale,
                visibleDate: this._visibleDate,
                icons:       this.options.icons,
            })
        );

        this._ui = {
            container:  this.node.firstChild,
            navigation: {
                container: this.node.querySelector('.datepicker-nav'),
                previous:  this.node.querySelector('.datepicker-nav-previous'),
                next:      this.node.querySelector('.datepicker-nav-next'),
                month:     this.node.querySelector('.datepicker-nav-month'),
                year:      this.node.querySelector('.datepicker-nav-year'),
            },
            body:       {
                dates:    this.node.querySelector('.datepicker-dates'),
                days:     this.node.querySelector('.datepicker-days'),
                weekdays: this.node.querySelector('.datepicker-weekdays'),
                months:   this.node.querySelector('.datepicker-months'),
                years:    this.node.querySelector('.datepicker-years'),
            },
        };
    }

    _bindEvents() {

        document.addEventListener('keydown', (e) => {
            if (this._focus) {
                switch (e.keyCode || e.which) {
                    case 37:
                        this.onPreviousDatePicker(e);
                        break;
                    case 39:
                        this.onNextDatePicker(e);
                        break;
                }
            }
        });

        // Bind year navigation events
        if (this._ui.navigation.previous) {
            this._clickEvents.forEach((clickEvent) => {
                this._ui.navigation.previous.addEventListener(clickEvent, this.onPreviousDatePicker);
            });
        }
        if (this._ui.navigation.next) {
            this._clickEvents.forEach((clickEvent) => {
                this._ui.navigation.next.addEventListener(clickEvent, this.onNextDatePicker);
            });
        }
        if (this._ui.navigation.month) {
            this._clickEvents.forEach((clickEvent) => {
                this._ui.navigation.month.addEventListener(clickEvent, this.onSelectMonthDatePicker);
            });
        }
        if (this._ui.navigation.year) {
            this._clickEvents.forEach((clickEvent) => {
                this._ui.navigation.year.addEventListener(clickEvent, this.onSelectYearDatePicker);
            });
        }

        const months = this._ui.body.months.querySelectorAll('.calendar-month') || [];
        months.forEach((month) => {
            this._clickEvents.forEach((clickEvent) => {
                month.addEventListener(clickEvent, this.onMonthClickDatePicker);
            });
        });

        const years = this._ui.body.years.querySelectorAll('.calendar-year') || [];
        years.forEach((year) => {
            this._clickEvents.forEach((clickEvent) => {
                year.addEventListener(clickEvent, this.onYearClickDatePicker);
            });
        });
    }

    /**
     * Bind events on each Day item
     * @method _bindDaysEvents
     * @return {void}
     */
    _bindDaysEvents() {
        [].forEach.call(this._ui.days, (day) => {
            this._clickEvents.forEach((clickEvent) => {
                // if not in range, no click action
                // if in this month, select the date
                // if out of this month, jump to the date
                const onClick = !this._isValidDate(new Date(day.dataset.date), this.min, this.max) ? null : this.onDateClickDatePicker;
                day.addEventListener(clickEvent, onClick);
            });

            day.addEventListener('hover', (e) => {
                e.preventDEfault();
            });
        });
    }

    _renderDays() {

        const start = dateFns.startOfWeek(dateFns.startOfMonth(this._visibleDate), { weekStartsOn: this.options.weekStart }); // first day of current month view
        const end   = dateFns.endOfWeek(dateFns.endOfMonth(this._visibleDate), { weekStartsOn: this.options.weekStart }); // last day of current month view

        // get all days and whether they are within the current month and range
        const days = new Array(dateFns.differenceInDays(end, start) + 1).fill(start).map((s, i) => {

            const theDate     = dateFns.startOfDay(dateFns.addDays(s, i));
            const isThisMonth = dateFns.isSameMonth(this._visibleDate, theDate);
            const isInRange   = this.options.isRange && this.end && dateFns.isWithinInterval(theDate, { start: dateFns.startOfDay(this.start), end: dateFns.endOfDay(this.end) });
            let isDisabled    = this.max ? dateFns.isAfter(dateFns.startOfDay(theDate), dateFns.endOfDay(this.max)) : false;
            isDisabled        = !isDisabled && this.min ? dateFns.isBefore(dateFns.startOfDay(theDate), dateFns.startOfDay(this.min)) : isDisabled;
            let isHighlighted = false;

            // Disabled Dates
            if (Array.isArray(this.disabledDates)) {
                for (let j = 0; j < this.disabledDates.length; j++) {
                    if (theDate.toDateString() === this.disabledDates[j].toDateString()) {
                        isDisabled = true;
                    }
                }
            } else {
                if (type.isFunction(this.disabledDates)) {
                    if (this.disabledDates(this, theDate)) {
                        isDisabled = true;
                    }
                }
            }

            // Highlighted Dates
            if (Array.isArray(this.highlightedDates)) {
                for (let j = 0; j < this.highlightedDates.length; j++) {
                    if (theDate.toDateString() === this.highlightedDates[j].toDateString()) {
                        isHighlighted = true;
                    }
                }
            } else {
                if (type.isFunction(this.highlightedDates)) {
                    if (this.highlightedDates(this, theDate)) {
                        isHighlighted = true;
                    }
                }
            }

            // Disabled Weekdays
            if (this.disabledWeekDays) {
                this.disabledWeekDays.forEach((day) => {
                    if (dateFns.getDay(theDate) == day) {
                        isDisabled = true;
                    }
                });
            }

            return {
                date:          theDate,
                isRange:       this.options.isRange,
                isToday:       dateFns.isToday(theDate),
                isStartDate:   dateFns.isEqual(dateFns.startOfDay(this.start), theDate),
                isEndDate:     dateFns.isEqual(dateFns.startOfDay(this.end), theDate),
                isDisabled:    isDisabled,
                isThisMonth,
                isHighlighted: isHighlighted,
                isInRange,
            };

        });

        this._ui.body.dates.appendChild(document.createRange().createContextualFragment(templateDays(days)));
        this._ui.days = this._ui.body.dates.querySelectorAll('.datepicker-date');
        this._bindDaysEvents();

    }

    _select(date = undefined, emit = true) {

        if (!type.isDate(date)) {
            date = utils.newDate(date, this.format, 'yyyy-MM-dd');
        }

        if (this.options.isRange && (!this._isValidDate(this.start) || (this._isValidDate(this.start) && this._isValidDate(this.end)))) {
            this.start = date;
            this.end   = undefined;
            if (emit) this.emit('select:start', this);
        } else if (this.options.isRange && !this._isValidDate(this.end)) {
            if (dateFns.isBefore(date, this.start)) {
                this.end   = this.start;
                this.start = date;
                if (emit) this.emit('select', this);
            } else if (dateFns.isAfter(date, this.start)) {
                this.end = date;
                if (emit) this.emit('select', this);
            } else if (this.options.allowSameDayRange) {
                this.end = date;
                if (emit) this.emit('select', this);
            } else {
                this.start = date;
                this.end   = undefined;
                if (emit) this.emit('select:start', this);
            }
        } else {
            this.start = date;
            this.end   = undefined;
            if (emit) this.emit('select', this);
        }

        this._setVisibleDate(this.start);

        if (this.options.isRange && this._isValidDate(this.start) && this._isValidDate(this.end)) {
            new Array(dateFns.differenceInDays(this.end, this.start) + 1).fill(this.start).map((s, i) => {
                const theDate     = dateFns.addDays(s, i);
                const dateElement = this._ui.body.dates.querySelector(`.datepicker-date[data-date="${theDate.toString()}"]`);
                if (dateElement) {
                    if (dateFns.isEqual(this.start, theDate)) {
                        dateElement.classList.add('datepicker-range-start');
                    }
                    if (dateFns.isEqual(this.end, theDate)) {
                        dateElement.classList.add('datepicker-range-end');
                    }
                    dateElement.classList.add('datepicker-range');
                }
            });
        }

    }

    _isValidDate(date, min, max) {
        try {
            if (!date) {
                return false;
            }
            if (dateFns.isValid(date)) {
                if (!min && !max) {
                    return true;
                }
                if (min && max) {
                    return dateFns.isWithinInterval(date, { start: min, end: max });
                }
                if (max) {
                    return dateFns.isBefore(date, max) || dateFns.isEqual(date, max);
                }
                return dateFns.isAfter(date, min) || dateFns.isEqual(date, min);
            } else {
                return false;
            }
        } catch (e) {
            return false;
        }
    }

    _setVisibleDate(date) {

        if (!date) return;

        if (this.min) date = dateFns.max([date, this.min]);
        if (this.max) date = dateFns.min([date, this.max]);

        this._visibleDate = date;

    }

    _togglePreviousButton(active = true) {
        if (!active) {
            this._ui.navigation.previous.setAttribute('disabled', 'disabled');
        } else {
            this._ui.navigation.previous.removeAttribute('disabled');
        }
    }

    _toggleNextButton(active = true) {
        if (!active) {
            this._ui.navigation.next.setAttribute('disabled', 'disabled');
        } else {
            this._ui.navigation.next.removeAttribute('disabled');
        }
    }

}
