/*!
 * @authors yusen
 * @date    2017-01-04 21:34:19
 * @github  https://github.com/yscoder/Calendar
 */
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define('calendar', ['jquery'], factory);
    } else if (typeof exports === 'object') {
        module.exports = factory(require('jquery'));
    } else {
        factory(root.jQuery);
    }
}(this, function ($) {

    // default config

    var defaults = {

        // 螳ス蠎ヲ
        width: 280,
        // 鬮伜コヲ, 荳榊桁蜷ォ螟エ驛ィ�悟、エ驛ィ蝗コ螳夐ォ伜コヲ
        height: 280,

        zIndex: 1,

        // selector
        // 隶セ鄂ョ隗ヲ蜿第仞遉コ逧��邏��御クコnull譌カ鮟倩ョ、譏セ遉コ
        trigger: null,

        // 蛛冗ァサ菴咲スョ�悟庄隶セ豁」雍溷€シ
        // trigger 隶セ鄂ョ譌カ逕滓譜
        offset: [0, 1],

        // 閾ェ螳壻ケ臥アサ�檎畑莠朱㍾蜀呎�キ蠑�
        customClass: '',

        // 譏セ遉コ隗�崟
        // 蜿ッ騾会シ單ate, month
        view: 'date',

        // 鮟倩ョ、譌・譛滉クコ蠖灘燕譌・譛�
        date: new Date(),
        format: 'yyyy/mm/dd',

        // 荳€蜻ィ逧�ャャ荳€螟ゥ
        // 0陦ィ遉コ蜻ィ譌・�御セ晄ャ。邀サ謗ィ
        startWeek: 0,

        // 譏滓悄譬シ蠑�
        weekArray: ['譌・', '荳€', '莠�', '荳�', '蝗�', '莠�', '蜈ュ'],

        // 譛井サス譬シ蠑�
        monthArray: ['1譛�', '2譛�', '3譛�', '4譛�', '5譛�', '6譛�', '7譛�', '8譛�', '9譛�', '10譛�', '11譛�', '12譛�'],

        // 隶セ鄂ョ騾画叫闌�峩
        // 譬シ蠑擾シ喙蠑€蟋区律譛�, 扈捺據譌・譛歉
        // 蠑€蟋区律譛滉クコ遨コ�悟�譌�荳企剞�帷サ捺據譌・譛滉クコ遨コ�悟�譌�荳矩剞
        // 螯りョセ鄂ョ2015蟷エ11譛�23譌・莉・蜑堺ク榊庄騾会シ喙new Date(), null] or ['2015/11/23']
        selectedRang: null,

        // 譌・譛溷�閨疲焚謐ョ [{ date: string, value: object }, ... ]
        // 譌・譛滓�シ蠑丈ク� format 荳€閾エ
        // 螯� [ {date: '2015/11/23', value: '髱「隸�'} ]
        data: null,

        // 螻慕、コ蜈ウ閨疲焚謐ョ
        // 譬シ蠑丞喧蜿よ焚�嘴m}隗�崟�鶏d}譌・譛滂シ鶏v}value
        // 隶セ鄂ョ false 陦ィ遉コ荳肴仞遉コ
        label: '{d}\n{v}',

        // 蛻�困蟄礼ャヲ
        prev: '<',
        next: '>',

        // 蛻�困隗�崟
        // 蜿よ焚�嘛iew, y, m
        viewChange: $.noop,

        // view: 隗�崟
        // date: 荳榊酔隗�崟霑泌屓荳榊酔逧�€シ
        // value: 譌・譛溷�閨疲焚謐ョ
        onSelected: function (view, date, value) {
            // body...
        },

        // 蜿よ焚蜷御ク�
        onMouseenter: $.noop,

        onClose: $.noop
    },

        // static variable

        ACTION_NAMESPACE = 'data-calendar-',

        DISPLAY_VD = '[' + ACTION_NAMESPACE + 'display-date]',
        DISPLAY_VM = '[' + ACTION_NAMESPACE + 'display-month]',

        ARROW_DATE = '[' + ACTION_NAMESPACE + 'arrow-date]',
        ARROW_MONTH = '[' + ACTION_NAMESPACE + 'arrow-month]',

        ITEM_DAY = ACTION_NAMESPACE + 'day',
        ITEM_MONTH = ACTION_NAMESPACE + 'month',

        DISABLED = 'disabled',
        MARK_DATA = 'markData',

        VIEW_CLASS = {
            date: 'calendar-d',
            month: 'calendar-m'
        },

        OLD_DAY_CLASS = 'old',
        NEW_DAY_CLASS = 'new',
        TODAY_CLASS = 'now',
        SELECT_CLASS = 'selected',
        MARK_DAY_HTML = '<i class="dot"></i>',
        DATE_DIS_TPL = '{year}/<span class="m">{month}</span>',

        ITEM_STYLE = 'style="width:{w}px;height:{h}px;line-height:{h}px"',
        WEEK_ITEM_TPL = '<li ' + ITEM_STYLE + '>{wk}</li>',
        DAY_ITEM_TPL = '<li ' + ITEM_STYLE + ' class="{class}" {action}="{date}">{value}</li>',
        MONTH_ITEM_TPL = '<li ' + ITEM_STYLE + ' ' + ITEM_MONTH + '>{m}</li>',

        TEMPLATE = [
            '<div class="calendar-inner">',
            '<div class="calendar-views">',
            '<div class="view view-date">',
            '<div class="calendar-hd">',
            '<a href="javascript:;" data-calendar-display-date class="calendar-display">',
            '{yyyy}/<span class="m">{mm}</span>',
            '</a>',
            '<div class="calendar-arrow">',
            '<span class="prev" data-calendar-arrow-date>{prev}</span>',
            '<span class="next" data-calendar-arrow-date>{next}</span>',
            '</div>',
            '</div>',
            '<div class="calendar-ct">',
            '<ol class="week">{week}</ol>',
            '<ul class="date-items"></ul>',
            '</div>',
            '</div>',
            '<div class="view view-month">',
            '<div class="calendar-hd">',
            '<a href="javascript:;" data-calendar-display-month class="calendar-display">{yyyy}</a>',
            '<div class="calendar-arrow">',
            '<span class="prev" data-calendar-arrow-month>{prev}</span>',
            '<span class="next" data-calendar-arrow-month>{next}</span>',
            '</div>',
            '</div>',
            '<ol class="calendar-ct month-items">{month}</ol>',
            '</div>',
            '</div>',
            '</div>',
            '<div class="calendar-label"><p>HelloWorld</p><i></i></div>'
        ],
        OS = Object.prototype.toString;

    // utils

    function isDate(obj) {
        return OS.call(obj) === '[object Date]';
    }

    function isString(obj) {
        return OS.call(obj) === '[object String]';
    }


    function getClass(el) {
        return el.getAttribute('class') || el.getAttribute('className');
    }

    // extension methods

    String.prototype.repeat = function (data) {
        return this.replace(/\{\w+\}/g, function (str) {
            var prop = str.replace(/\{|\}/g, '');
            return data[prop] || '';
        });
    }

    String.prototype.toDate = function () {
        var dt = new Date(),
            dot = this.replace(/\d/g, '').charAt(0),
            arr = this.split(dot);

        return new Date(parseInt(arr[0]), parseInt(arr[1]) - 1, parseInt(arr[2]));
    }

    Date.prototype.format = function (exp) {
        var y = this.getFullYear(),
            m = this.getMonth() + 1,
            d = this.getDate();

        return exp.replace('yyyy', y).replace('mm', m).replace('dd', d);
    }

    Date.prototype.isSame = function (y, m, d) {
        if (isDate(y)) {
            var dt = y;
            y = dt.getFullYear();
            m = dt.getMonth() + 1;
            d = dt.getDate();
        }
        return this.getFullYear() === y && this.getMonth() + 1 === m && this.getDate() === d;
    }

    Date.prototype.add = function (n) {
        this.setDate(this.getDate() + n);
    }

    Date.prototype.minus = function (n) {
        this.setDate(this.getDate() - n);
    }

    Date.prototype.clearTime = function (n) {
        this.setHours(0);
        this.setSeconds(0);
        this.setMinutes(0);
        this.setMilliseconds(0);
        return this;
    }

    Date.isLeap = function (y) {
        return (y % 100 !== 0 && y % 4 === 0) || (y % 400 === 0);
    }

    Date.getDaysNum = function (y, m) {
        var num = 31;

        switch (m) {
            case 2:
                num = this.isLeap(y) ? 29 : 28;
                break;
            case 4:
            case 6:
            case 9:
            case 11:
                num = 30;
                break;
        }
        return num;
    }

    Date.getSiblingsMonth = function (y, m, n) {
        var d = new Date(y, m - 1);
        d.setMonth(m - 1 + n);
        return {
            y: d.getFullYear(),
            m: d.getMonth() + 1
        };
    }

    Date.getPrevMonth = function (y, m, n) {
        return this.getSiblingsMonth(y, m, 0 - (n || 1));
    }

    Date.getNextMonth = function (y, m, n) {
        return this.getSiblingsMonth(y, m, n || 1);
    }

    Date.tryParse = function (obj) {
        if (!obj) {
            return obj;
        }
        return isDate(obj) ? obj : obj.toDate();
    }


    // Calendar class

    function Calendar(element, options) {
        this.$element = $(element);
        this.options = $.extend({}, $.fn.calendar.defaults, options);
        this.$element.addClass('calendar ' + this.options.customClass);
        this.width = this.options.width;
        this.height = this.options.height;
        this.date = this.options.date;
        this.selectedRang = this.options.selectedRang;
        this.data = this.options.data;
        this.init();
    }

    Calendar.prototype = {
        constructor: Calendar,
        getDayAction: function (day) {
            var action = ITEM_DAY;
            if (this.selectedRang) {
                var start = Date.tryParse(this.selectedRang[0]),
                    end = Date.tryParse(this.selectedRang[1]);

                if ((start && day < start.clearTime()) || (end && day > end.clearTime())) {
                    action = DISABLED;
                }
            }

            return action;
        },
        getDayData: function (day) {
            var ret, data = this.data;

            if (data) {

                for (var i = 0, len = data.length; i < len; i++) {
                    var item = data[i];

                    if (day.isSame(Date.tryParse(item.date))) {
                        ret = item.value;
                    }
                }
            }

            return ret;
        },
        getDayItem: function (y, m, d, f) {
            var dt = this.date,
                idt = new Date(y, m - 1, d),
                data = {
                    w: this.width / 7,
                    h: this.height / 7,
                    value: d
                },
                markData,
                $item;

            var selected = dt.isSame(y, m, d) ? SELECT_CLASS : '';
            if (f === 1) {
                data['class'] = OLD_DAY_CLASS;
            } else if (f === 3) {
                data['class'] = NEW_DAY_CLASS;
            } else {
                data['class'] = '';
            }

            if (dt.isSame(y, m, d)) {
                data['class'] += ' ' + TODAY_CLASS;
            }

            data.date = idt.format(this.options.format);
            data.action = this.getDayAction(idt);
            markData = this.getDayData(idt);

            $item = $(DAY_ITEM_TPL.repeat(data));

            if (markData) {
                $item.data(MARK_DATA, markData);
                $item.html(d + MARK_DAY_HTML);
            }

            return $item;
        },
        getDaysHtml: function (y, m) {
            var year, month, firstWeek, daysNum, prevM, prevDiff,
                dt = this.date,
                $days = $('<ol class="days"></ol>');

            if (isDate(y)) {
                year = y.getFullYear();
                month = y.getMonth() + 1;
            } else {
                year = Number(y);
                month = Number(m);
            }

            firstWeek = new Date(year, month - 1, 1).getDay() || 7;
            prevDiff = firstWeek - this.options.startWeek;
            daysNum = Date.getDaysNum(year, month);
            prevM = Date.getPrevMonth(year, month);
            prevDaysNum = Date.getDaysNum(year, prevM.m);
            nextM = Date.getNextMonth(year, month);
            // month flag
            var PREV_FLAG = 1,
                CURR_FLAG = 2,
                NEXT_FLAG = 3,
                count = 0;

            for (var p = prevDaysNum - prevDiff + 1; p <= prevDaysNum; p++ , count++) {

                $days.append(this.getDayItem(prevM.y, prevM.m, p, PREV_FLAG));
            }

            for (var c = 1; c <= daysNum; c++ , count++) {
                $days.append(this.getDayItem(year, month, c, CURR_FLAG));
            }

            for (var n = 1, nl = 42 - count; n <= nl; n++) {

                $days.append(this.getDayItem(nextM.y, nextM.m, n, NEXT_FLAG));
            }

            return $('<li></li>').width(this.options.width).append($days);
        },
        getWeekHtml: function () {
            var week = [],
                weekArray = this.options.weekArray,
                start = this.options.startWeek,
                len = weekArray.length,
                w = this.width / 7,
                h = this.height / 7;

            for (var i = start; i < len; i++) {
                week.push(WEEK_ITEM_TPL.repeat({
                    w: w,
                    h: h,
                    wk: weekArray[i]
                }));
            }

            for (var j = 0; j < start; j++) {
                week.push(WEEK_ITEM_TPL.repeat({
                    w: w,
                    h: h,
                    wk: weekArray[j]
                }));
            }

            return week.join('');
        },
        getMonthHtml: function () {
            var monthArray = this.options.monthArray,
                month = [],
                w = this.width / 4,
                h = this.height / 4,
                i = 0;

            for (; i < 12; i++) {
                month.push(MONTH_ITEM_TPL.repeat({
                    w: w,
                    h: h,
                    m: monthArray[i]
                }));
            }

            return month.join('');
        },
        setMonthAction: function (y) {
            var m = this.date.getMonth() + 1;

            this.$monthItems.children().removeClass(TODAY_CLASS);
            if (y === this.date.getFullYear()) {
                this.$monthItems.children().eq(m - 1).addClass(TODAY_CLASS);
            }
        },
        fillStatic: function () {
            var staticData = {
                prev: this.options.prev,
                next: this.options.next,
                week: this.getWeekHtml(),
                month: this.getMonthHtml()
            };

            this.$element.html(TEMPLATE.join('').repeat(staticData));
        },
        updateDisDate: function (y, m) {
            this.$disDate.html(DATE_DIS_TPL.repeat({
                year: y,
                month: m
            }));
        },
        updateDisMonth: function (y) {
            this.$disMonth.html(y);
        },
        fillDateItems: function (y, m) {
            var ma = [
                Date.getPrevMonth(y, m), {
                    y: y,
                    m: m
                },
                Date.getNextMonth(y, m)
            ];

            this.$dateItems.html('');
            for (var i = 0; i < 3; i++) {
                var $item = this.getDaysHtml(ma[i].y, ma[i].m);
                this.$dateItems.append($item);
            }

        },
        hide: function (view, date, data) {
            this.$trigger.val(date.format(this.options.format));
            this.options.onClose.call(this, view, date, data);
            this.$element.hide();
        },
        setPosition: function () {
            var post = this.$trigger.offset();
            var offs = this.options.offset;

            this.$element.css({
                left: (post.left + offs[0]) + 'px',
                top: (post.top + this.$trigger.outerHeight() + offs[1]) + 'px'
            })
        },
        trigger: function () {

            this.$trigger = $(this.options.trigger);

            var _this = this,
                $this = _this.$element;

            $this.addClass('calendar-modal').css('zIndex', _this.options.zIndex);

            $(document).click(function (e) {
                if (_this.$trigger[0] != e.target && !$.contains($this[0], e.target)) {
                    $this.hide();
                }
            }).on('click', this.options.trigger, function () {
                this.$trigger = $(this);
                _this.setPosition();
                $this.show();
            })

            $(window).resize(function () {
                _this.setPosition();
            });
        },
        render: function () {
            this.$week = this.$element.find('.week');
            this.$dateItems = this.$element.find('.date-items');
            this.$monthItems = this.$element.find('.month-items');
            this.$label = this.$element.find('.calendar-label');
            this.$disDate = this.$element.find(DISPLAY_VD);
            this.$disMonth = this.$element.find(DISPLAY_VM);

            var y = this.date.getFullYear(),
                m = this.date.getMonth() + 1;

            this.updateDisDate(y, m);
            this.updateMonthView(y);

            this.fillDateItems(y, m);

            this.options.trigger && this.trigger();

        },
        setView: function (view) {
            this.$element.removeClass(VIEW_CLASS.date + ' ' + VIEW_CLASS.month)
                .addClass(VIEW_CLASS[view]);
            this.view = view;
        },
        updateDateView: function (y, m, dirc, cb) {
            m = m || this.date.getMonth() + 1;

            var _this = this,
                $dis = this.$dateItems,
                exec = {
                    prev: function () {
                        var pm = Date.getPrevMonth(y, m),
                            ppm = Date.getPrevMonth(y, m, 2),
                            $prevItem = _this.getDaysHtml(ppm.y, ppm.m);

                        m = pm.m;
                        y = pm.y;

                        $dis.animate({
                            marginLeft: 0
                        }, 300, 'swing', function () {
                            $dis.children(':last').remove();
                            $dis.prepend($prevItem).css('margin-left', '-100%');

                            $.isFunction(cb) && cb.call(_this);
                        });
                    },
                    next: function () {
                        var nm = Date.getNextMonth(y, m),
                            nnm = Date.getNextMonth(y, m, 2),
                            $nextItem = _this.getDaysHtml(nnm.y, nnm.m);

                        m = nm.m;
                        y = nm.y;

                        $dis.animate({
                            marginLeft: '-200%'
                        }, 300, 'swing', function () {
                            $dis.children(':first').remove();
                            $dis.append($nextItem).css('margin-left', '-100%');

                            $.isFunction(cb) && cb.call(_this);
                        });

                    }
                };


            if (dirc) {
                exec[dirc]();
            } else {
                this.fillDateItems(y, m);
            }

            this.updateDisDate(y, m);

            this.setView('date');

            return {
                y: y,
                m: m
            };
        },
        updateMonthView: function (y) {
            this.updateDisMonth(y);
            this.setMonthAction(y);
            this.setView('month');
        },
        getDisDateValue: function () {
            var arr = this.$disDate.html().split('/'),
                y = Number(arr[0]),
                m = Number(arr[1].match(/\d{1,2}/)[0]);

            return [y, m];
        },
        selectedDay: function (d, type) {
            var arr = this.getDisDateValue(),
                y = arr[0],
                m = arr[1],
                toggleClass = function () {
                    this.$dateItems.children(':eq(1)')
                        .find('[' + ITEM_DAY + ']:not(.' + NEW_DAY_CLASS + ', .' + OLD_DAY_CLASS + ')')
                        .removeClass(SELECT_CLASS)
                        .filter(function (index) {
                            return parseInt(this.innerHTML) === d;
                        }).addClass(SELECT_CLASS);
                };

            if (type) {
                var ret = this.updateDateView(y, m, {
                    'old': 'prev',
                    'new': 'next'
                }[type], toggleClass);
                y = ret.y;
                m = ret.m;
                this.options.viewChange('date', y, m);
            } else {
                toggleClass.call(this);
            }

            return new Date(y, m - 1, d);
        },
        showLabel: function (event, view, date, data) {
            var $lbl = this.$label;

            $lbl.find('p').html(this.options.label.repeat({
                m: view,
                d: date.format(this.options.format),
                v: data
            }).replace(/\n/g, '<br>'));

            var w = $lbl.outerWidth(),
                h = $lbl.outerHeight();

            $lbl.css({
                left: (event.pageX - w / 2) + 'px',
                top: (event.pageY - h - 20) + 'px',
                zIndex: this.options.zIndex + 1
            }).show();
        },
        hasLabel: function () {
            if (this.options.label) {
                $('body').append(this.$label);
                return true;
            }
            return false;
        },
        event: function () {
            var _this = this,
                vc = _this.options.viewChange;

            // view change
            _this.$element.on('click', DISPLAY_VD, function () {
                var arr = _this.getDisDateValue();
                _this.updateMonthView(arr[0], arr[1]);

                vc('month', arr[0], arr[1]);

            }).on('click', DISPLAY_VM, function () {
                var y = this.innerHTML;

                _this.updateDateView(y);
                vc('date', y);
            });

            // arrow
            _this.$element.on('click', ARROW_DATE, function () {
                var arr = _this.getDisDateValue(),
                    type = getClass(this),
                    y = arr[0],
                    m = arr[1];

                var d = _this.updateDateView(y, m, type, function () {
                    vc('date', d.y, d.m);
                });

            }).on('click', ARROW_MONTH, function () {

                var y = Number(_this.$disMonth.html()),
                    type = getClass(this);

                y = type === 'prev' ? y - 1 : y + 1;
                _this.updateMonthView(y);
                vc('month', y);
            });

            // selected
            _this.$element.on('click', '[' + ITEM_DAY + ']', function () {
                var d = parseInt(this.innerHTML),
                    cls = getClass(this),
                    type = /new|old/.test(cls) ? cls.match(/new|old/)[0] : '';

                var day = _this.selectedDay(d, type);

                _this.options.onSelected.call(this, 'date', day, $(this).data(MARK_DATA));

                _this.$trigger && _this.hide('date', day, $(this).data(MARK_DATA));

            }).on('click', '[' + ITEM_MONTH + ']', function () {
                var y = Number(_this.$disMonth.html()),
                    m = $(this).index() + 1;

                _this.updateDateView(y, m);
                vc('date', y, m);
                _this.options.onSelected.call(this, 'month', new Date(y, m - 1));
            });

            // hover
            _this.$element.on('mouseenter', '[' + ITEM_DAY + ']', function (e) {
                var $this = $(this),
                    day = $this.attr(ITEM_DAY).toDate();

                if (_this.hasLabel() && $this.data(MARK_DATA)) {
                    _this.showLabel(e, 'date', day, $this.data(MARK_DATA));
                }

                _this.options.onMouseenter.call(this, 'date', day, $this.data(MARK_DATA));
            }).on('mouseleave', '[' + ITEM_DAY + ']', function () {
                _this.$label.hide();
            });
        },
        resize: function () {
            var w = this.width,
                h = this.height,
                hdH = this.$element.find('.calendar-hd').outerHeight();

            this.$element.width(w).height(h + hdH)
                .find('.calendar-inner, .view')
                .css('width', w + 'px');

            this.$element.find('.calendar-ct').width(w).height(h);

        },
        init: function () {

            this.fillStatic();
            this.resize();
            this.render();
            this.view = this.options.view;
            this.setView(this.view);
            this.event();
        },
        setData: function (data) {
            this.data = data;

            if (this.view === 'date') {
                var d = this.getDisDateValue();
                this.fillDateItems(d[0], d[1]);
            } else if (this.view === 'month') {
                this.updateMonthView(this.$disMonth.html());
            }
        },
        setRang: function(data){
            this.selectedRang = data;
            this.setData()
        },
        setDate: function (date) {
            var dateObj = Date.tryParse(date);
            this.updateDateView(dateObj.getFullYear(), dateObj.getMonth() + 1);
            this.selectedDay(dateObj.getDate());
        },
        methods: function (name, args) {
            if (OS.call(this[name]) === '[object Function]') {
                return this[name].apply(this, args);
            }
        }
    };

    $.fn.calendar = function (options) {
        var calendar = this.data('calendar'),
            fn,
            args = [].slice.call(arguments);

        if (!calendar) {
            return this.each(function () {
                return $(this).data('calendar', new Calendar(this, options));
            });
        }
        if (isString(options)) {
            fn = options;
            args.shift();
            return calendar.methods(fn, args);
        }

        return this;
    }

    $.fn.calendar.defaults = defaults;

}));