/**
 * @fileoverview Factory module for control all other factory.
 * @author NHN Ent. FE Development Team <dl_javascript@nhnent.com>
 */
'use strict';

var util = require('tui-code-snippet'),
    Handlebars = require('handlebars-template-loader/runtime');
var dw = require('../common/dw'),
    datetime = require('../common/datetime'),
    Layout = require('../view/layout'),
    Drag = require('../handler/drag'),
    controllerFactory = require('./controller'),
    weekViewFactory = require('./weekView'),
    monthViewFactory = require('./monthView'),
    TZDate = require('../common/timezone').Date,
    config = require('../config'),
    timezone = require('../common/timezone');

var mmin = Math.min;

/**
 * @typedef {object} Schedule
 * @property {string} id - unique schedule id depends on calendar id
 * @property {string} calendarId - unique calendar id
 * @property {string} title - schedule title
 * @property {string} start - start time
 * @property {string} end - end time
 * @property {boolean} isAllDay - all day schedule
 * @property {string} category - schedule type('milestone', 'task', allday', 'time')
 * @property {string} dueDateClass - task schedule type string
 *                                   (any string value is ok and mandatory if category is 'task')
 * @property {boolean} isPending - in progress flag to do something like network job(The schedule will be transparent.)
 * @property {boolean} isFocused - focused schedule flag
 * @property {boolean} isVisible - schedule visibility flag
 * @property {boolean} isReadOnly - schedule read-only flag
 * @property {string} [color] - schedule text color
 * @property {string} [bgColor] - schedule background color
 * @property {string} [borderColor] - schedule left border color
 * @property {string} customStyle - schedule's custom css class
 * @property {any} raw - user data
 */

/**
 * @typedef {object} RenderRange - rendered range
 * @property {Date} start - start date
 * @property {Date} end - end date
 */

/**
 * @typedef {object} Options - calendar option object
 * @property {string} [cssPrefix] - CSS classname prefix
 *  @property {string} [defaultView='week'] - default view of calendar
 *  @property {string} [defaultDate=null] - default date to render calendar. if not supplied, use today.
 *  @property {object} [calendarColor] - preset calendar colors
 *   @property {string} [calendarColor.color] - calendar color
 *   @property {string} [calendarColor.bgColor] - calendar background color
 *   @property {string} [calendarColor.borderColor] - calendar left border color
 *   @property {boolean} [calendarColor.render] - immediately apply colors when setCalendarColor called.
 *  @property {boolean} [taskView=true] - show the milestone and task in weekly, daily view
 * @property {boolean} [scheduleView=true] - show the all day and time grid in weekly, daily view
 *  @property {object} [template] - template option
 *   @property {function} [template.milestoneTitle] - milestone title(at left column) template function
 *   @property {function} [template.milestone] - milestone template function
 *   @property {function} [template.taskTitle] - task title(at left column) template function
 *   @property {function} [template.task] - task template function
 *   @property {function} [template.alldayTitle] - allday title(at left column) template function
 *   @property {function} [template.allday] - allday template function
 *   @property {function} [template.time] - time template function
 *   @property {function} [template.monthMoreTitleDate] - month more layer title template function
 *   @property {function} [template.monthMoreClose] - month more layer close button template function
 *   @property {function} [template.monthGridHeader] - month grid header(date, decorator, title) template function
 *   @property {function} [template.monthGridFooter] - month grid footer(date, decorator, title) template function
 *   @property {function} [template.monthGridHeaderExceed] - month grid header(exceed schedule count) template function
 *   @property {function} [template.monthGridFooterExceed] - month grid footer(exceed schedule count) template function
 *   @property {function} [template.weekDayname] - weekly dayname template function
 *   @property {function} [template.monthDayname] - monthly dayname template function
 *  @property {object} [week] - options for week view
 *   @property {number} [week.startDayOfWeek=0] - start day of week
 *   @property {Array.<number>} [week.panelHeights] - each panel height px(Milestone, Task, Allday View Panel)
 *   @property {Array.<string>} [week.daynames] - day names in weekly and daily.
 * Default values are ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
 *   @property {boolean} [week.narrowWeekend=false] - make weekend column narrow(1/2 width)
 *   @property {boolean} [week.workweek=false] - show only 5 days except for weekend
 *   @property {string} [week.alldayViewType='scroll'] - set view type of allday panel. ('scroll'|'toggle')
 *  @property {object} [month] - options for month view
 *   @property {Array.<string>} [month.daynames] - day names in monthly.
 * Default values are ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
 *   @property {number} [month.startDayOfWeek=0] - start day of week
 *   @property {boolean} [month.narrowWeekend=false] - make weekend column narrow(1/2 width)
 *   @property {boolean} [month.visibleWeeksCount=6] - visible week count in monthly(0 or null are same with 6)
 *   @property {number} [month.visibleScheduleCount] - visible schedule count in monthly grid
 *   @property {object} [month.moreLayerSize] - more layer size
 *    @property {object} [month.moreLayerSize.width=null] - css width value(px, 'auto').
 *                                                           The default value 'null' is to fit a grid cell.
 *    @property {object} [month.moreLayerSize.height=null] - css height value(px, 'auto').
 *                                                            The default value 'null' is to fit a grid cell.
 *   @property {object} [month.grid] - grid's header and footer information
 *    @property {object} [month.grid.header] - grid's header informatioin
 *     @property {number} [month.grid.header.height=34] - grid's header height
 *    @property {object} [month.grid.footer] - grid's footer informatioin
 *     @property {number} [month.grid.footer.height=34] - grid's footer height
 *  @property {Array.<Schedule>} [schedules] - array of Schedule data for add calendar after initialize.
 */

/**
 * @typedef {class} CustomEvents
 * https://nhnent.github.io/tui.code-snippet/latest/tui.util.CustomEvents.html
 */

/**
 * @typedef {object} TimeCreationGuide - time creation guide instance to present selected time period
 * @property {HTMLElement} guideElement - guide element
 * @property {Object.<string, HTMLElement>} guideElements - map by key. It can be used in monthly view
 * @property {function} clearGuideElement - hide the creation guide
 * @example
 * calendar.on('beforeCreateSchedule', function(event) {
 *     var guide = event.guide;
 *     // use guideEl$'s left, top to locate your schedule creation popup
 *     var guideEl$ = guide.guideElement ?
 *          guide.guideElement : guide.guideElements[Object.keys(guide.guideElements)[0]];
 *
 *     // after that call this to hide the creation guide
 *     guide.clearGuideElement();
 * });
 */

/**
 * Calendar class
 * @constructor
 * @mixes CustomEvents
 * @param {HTMLElement|string} container - container element or selector id
 * @param {Options} options - calendar options
 * @example
 * var calendar = new tui.Calendar(document.getElementById('calendar'), {
 *     defaultView: 'week',
 *     taskView: true,
 *     scheduleView: true,
 *     template: {
 *         milestone: function(schedule) {
 *             return '<span style="color:red;"><i class="fa fa-flag"></i> ' + schedule.title + '</span>';
 *         },
 *         milestoneTitle: function() {
 *             return 'Milestone';
 *         },
 *         task: function(schedule) {
 *             return '&nbsp;&nbsp;#' + schedule.title;
 *         },
 *         taskTitle: function() {
 *             return '<label><input type="checkbox" />Task</label>';
 *         },
 *         allday: function(schedule) {
 *             return schedule.title + ' <i class="fa fa-refresh"></i>';
 *         },
 *         alldayTitle: function() {
 *             return 'All Day';
 *         },
 *         time: function(schedule) {
 *             return schedule.title + ' <i class="fa fa-refresh"></i>' + schedule.start;
 *         }
 *     },
 *     month: {
 *         daynames: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
 *         startDayOfWeek: 0,
 *         narrowWeekend: true
 *     },
 *     week: {
 *         daynames: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
 *         panelHeights: [80, 80, 120],
 *         startDayOfWeek: 0,
 *         narrowWeekend: true
 *     }
 * });
 */
function Calendar(container, options) {
    var opt;

    if (util.isString(container)) {
        container = document.querySelector(container);
    }

    /**
     * calendar options
     * @type {Options}
     */
    this.options = opt = util.extend({
        calendarColor: {},
        groupFunc: function(viewModel) {
            var model = viewModel.model;

            if (model.category === 'time' && (model.end - model.start > datetime.MILLISECONDS_PER_DAY)) {
                return 'allday';
            }

            return model.category;
        },
        controller: null,
        defaultView: 'week',
        taskView: true,
        scheduleView: true,
        defaultDate: new TZDate(),
        template: util.extend({
            allday: null,
            time: null
        }, util.pick(options, 'template') || {}),
        week: util.extend({}, util.pick(options, 'week') || {}),
        month: util.extend({}, util.pick(options, 'month') || {}),
        schedules: []
    }, options);

    this.options.week = util.extend({
        startDayOfWeek: 0,
        workweek: false
    }, util.pick(this.options, 'week') || {});

    this.options.month = util.extend({
        scheduleFilter: function(schedule) {
            return Boolean(schedule.isVisible) &&
                (schedule.category === 'allday' || schedule.category === 'time');
        }
    }, util.pick(options, 'month') || {});

    /**
     * Calendar color map
     * @type {object}
     * @private
     */
    this.calendarColor = opt.calendarColor;

    /**
     * @type {HTMLElement}
     * @private
     */
    this.container = container;

    /**
     * Current rendered date
     * @type {Date}
     * @readonly
     */
    this.renderDate = opt.defaultDate;

    /**
     * start and end date of weekly, monthly
     * @type {RenderRange}
     * @readonly
     */
    this.renderRange = {
        start: null,
        end: null
    };

    /**
     * base controller
     * @type {Base}
     * @private
     */
    this.controller = opt.controller || this.createController();

    /**
     * layout view (layout manager)
     * @type {Layout}
     * @private
     */
    this.layout = new Layout(container);

    /**
     * global drag handler
     * @type {Drag}
     * @private
     */
    this.dragHandler = new Drag({distance: 10}, this.layout.container);

    /**
     * current rendered view name. ('day', 'week', 'month')
     * @type {string}
     * @default 'week'
     * @readonly
     */
    this.viewName = opt.defaultView;

    /**
     * previous rendered view name
     * @type {string}
     * @private
     */
    this.prevViewName = this.viewName;

    /**
     * Refresh method. it can be ref different functions for each view modes.
     * @type {function}
     * @private
     */
    this.refreshMethod = null;

    /**
     * Scroll to now. It can be called for 'week', 'day' view modes.
     * @type {function}
     * @private
     */
    this.scrollToNowMethod = null;

    this.initialize();
}

/**
 * Create controller instance
 * @returns {Base} controller instance
 * @private
 */
Calendar.prototype.createController = function() {
    return controllerFactory(this.options);
};

/**
 * Create week view instance by dependent module instances
 * @param {Base} controller - controller
 * @param {HTMLElement} container - container element
 * @param {Drag} dragHandler - global drag handler
 * @param {object} options - options for week view
 * @returns {Week} week view instance
 * @private
 */
Calendar.prototype.createWeekView = function(controller, container, dragHandler, options) {
    return weekViewFactory(
        controller,
        container,
        dragHandler,
        options
    );
};

/**
 * Create week view instance by dependent module instances
 * @param {Base} controller - controller
 * @param {HTMLElement} container - container element
 * @param {Drag} dragHandler - global drag handler
 * @param {object} options - options for week view
 * @returns {Month} month view instance
 * @private
 */
Calendar.prototype.createMonthView = function(controller, container, dragHandler, options) {
    return monthViewFactory(
        controller,
        container,
        dragHandler,
        options
    );
};

/**
 * destroy calendar instance.
 */
Calendar.prototype.destroy = function() {
    this.dragHandler.destroy();
    this.controller.off();
    this.layout.clear();
    this.layout.destroy();

    util.forEach(this.options.template, function(func, name) {
        if (func) {
            Handlebars.unregisterHelper(name + '-tmpl');
        }
    });

    this.options = this.renderDate = this.controller =
        this.layout = this.dragHandler = this.viewName = this.prevViewName =
        this.refreshMethod = this.scrollToNowMethod = null;
};

/**
 * Initialize calendar
 * @private
 */
Calendar.prototype.initialize = function() {
    var controller = this.controller,
        viewName = this.viewName,
        opt = this.options;

    this.layout.controller = controller;

    if (opt.schedules && opt.schedules.length) {
        this.createSchedules(opt.schedules, true);
    }

    util.forEach(opt.template, function(func, name) {
        if (func) {
            Handlebars.registerHelper(name + '-tmpl', func);
        }
    });

    this.toggleView(viewName, true);
};

/**********
 * CRUD Methods
 **********/

/**
 * Create schedules and render calendar.
 * @param {Array.<Schedule>} schedules - schedule data list
 * @param {boolean} [silent=false] - no auto render after creation when set true
 * @example
 * calendar.createSchedules([
 *     {
 *         id: '1',
 *         calendarId: '1',
 *         title: 'my schedule',
 *         category: 'time',
 *         dueDateClass: '',
 *         start: '2018-01-18T22:30:00+09:00',
 *         end: '2018-01-19T02:30:00+09:00'
 *     },
 *     {
 *         id: '2',
 *         calendarId: '1',
 *         title: 'second schedule',
 *         category: 'time',
 *         dueDateClass: '',
 *         start: '2018-01-18T17:30:00+09:00',
 *         end: '2018-01-19T17:31:00+09:00'
 *     }
 * ]);
 */
Calendar.prototype.createSchedules = function(schedules, silent) {
    var calColor = this.calendarColor;

    util.forEach(schedules, function(obj) {
        var color = calColor[obj.calendarId];

        if (color) {
            obj.color = color.color;
            obj.bgColor = color.bgColor;
            obj.borderColor = color.borderColor;
        }
    });

    this.controller.createSchedules(schedules, silent);

    if (!silent) {
        this.render();
    }
};

/**
 * Get schedule by schedule id and calendar id.
 * @param {string} id - ID of schedule
 * @param {string} calendarId - calendarId of schedule
 * @returns {Schedule} schedule object
 * @example
 * var schedule = calendar.getSchedule(scheduleId, calendarId);
 * console.log(schedule.title);
 */
Calendar.prototype.getSchedule = function(id, calendarId) {
    return this.controller.schedules.single(function(model) {
        return model.id === id && model.calendarId === calendarId;
    });
};

/**
 * Update the schedule
 * @param {string} id - ID of schedule to update
 * @param {string} calendarId - calendarId of schedule to update
 * @param {Schedule} scheduleData - schedule data to update
 * @example
 * calendar.on('beforeUpdateSchedule', function(event) {
 *     var schedule = event.schedule;
 *     var startTime = event.start;
 *     var endTime = event.end;
 *     calendar.updateSchedule(schedule.id, schedule.calendarId, {
 *         start: startTime,
 *         end: endTime
 *     });
 * });
 */
Calendar.prototype.updateSchedule = function(id, calendarId, scheduleData) {
    var ctrl = this.controller,
        ownSchedules = ctrl.schedules,
        schedule = ownSchedules.single(function(model) {
            return model.id === id && model.calendarId === calendarId;
        });

    if (schedule) {
        ctrl.updateSchedule(schedule, scheduleData);
        this.render();
    }
};

/**
 * Delete schedule.
 * @fires Calendar#beforeDeleteSchedule
 * @param {string} id - ID of schedule to delete
 * @param {string} calendarId - calendarId of schedule to delete
 */
Calendar.prototype.deleteSchedule = function(id, calendarId) {
    var ctrl = this.controller,
        ownSchedules = ctrl.schedules,
        schedule = ownSchedules.single(function(model) {
            return model.id === id && model.calendarId === calendarId;
        });

    if (!schedule) {
        return;
    }

    /**
     * Fire this event when delete a schedule.
     * @event Calendar#beforeDeleteSchedule
     * @type {object}
     * @property {Schedule} schedule - schedule instance to delete
     * @example
     * calendar.on('beforeDeleteSchedule', function() {
     *     alert('The schedule is removed.');
     * });
     */
    this.fire('beforeDeleteSchedule', {
        schedule: schedule
    });

    ctrl.deleteSchedule(schedule);
    this.render();
};

/**********
 * Private Methods
 **********/

/**
 * Set child view's options recursively
 * @param {View} view - parent view
 * @param {function} func - option manipulate function
 * @private
 */
Calendar.prototype.setOptionRecurseively = function(view, func) {
    view.recursive(function(childView) {
        var opt = childView.options;

        if (!opt) {
            return;
        }

        func(opt);
    });
};

/**
 * @param {string|Date} date - date to show in calendar
 * @param {number} [startDayOfWeek=0] - start day of week
 * @param {boolean} [workweek=false] - only show work week
 * @returns {array} render range
 * @private
 */
Calendar.prototype.getWeekDayRange = function(date, startDayOfWeek, workweek) {
    var day, start, end, range,
        msFrom = datetime.millisecondsFrom;

    startDayOfWeek = (startDayOfWeek || 0); // eslint-disable-line
    date = util.isDate(date) ? date : new TZDate(date);
    day = date.getDay();

    // calculate default render range first.
    start = new TZDate(
        Number(date) -
        msFrom('day', day) +
        msFrom('day', startDayOfWeek)
    );

    end = new TZDate(Number(start) + msFrom('day', 6));

    if (day < startDayOfWeek) {
        start = new TZDate(Number(start) - msFrom('day', 7));
        end = new TZDate(Number(end) - msFrom('day', 7));
    }

    if (workweek) {
        range = datetime.range(
            datetime.start(start),
            datetime.end(end),
            datetime.MILLISECONDS_PER_DAY
        );

        range = util.filter(range, function(weekday) {
            return !datetime.isWeekend(weekday.getDay());
        });

        start = range[0];
        end = range[range.length - 1];
    }

    return [start, end];
};

/**
 * Toggle schedules visibility by calendar ID
 * @param {string} calendarId - calendar id value
 * @param {boolean} toHide - set true to hide schedules
 * @param {boolean} render - set true then render after change visible property each models
 * @private
 */
Calendar.prototype._toggleSchedulesByCalendarID = function(calendarId, toHide, render) {
    var ownSchedules = this.controller.schedules;

    calendarId = util.isArray(calendarId) ? calendarId : [calendarId];

    ownSchedules.each(function(schedule) {
        if (~util.inArray(schedule.calendarId, calendarId)) {
            schedule.set('isVisible', !toHide);
        }
    });

    if (render) {
        this.render();
    }
};

/**********
 * General Methods
 **********/

/**
 * Render the calendar.
 * @example
 * var silent = true;
 * calendar.clear();
 * calendar.createSchedules(schedules, silent);
 * calendar.render();
 */
Calendar.prototype.render = function() {
    this.layout.render();
};

/**
 * Delete all schedules and clear view.
 * @example
 * calendar.clear();
 * calendar.createSchedules(schedules, true);
 * calendar.render();
 */
Calendar.prototype.clear = function() {
    this.controller.clearSchedules();
    this.render();
};

/**
 * Scroll to now in daily, weekly view
 * @example
 * function onNewSchedules(schedules) {
 *     calendar.createSchedules(schedules);
 *     if (calendar.viewName !== 'month') {
 *         calendar.scrollToNow();
 *     }
 * }
 */
Calendar.prototype.scrollToNow = function() {
    if (this.scrollToNowMethod) {
        this.scrollToNowMethod();
    }
};

/**
 * Refresh the calendar layout.
 * @example
 * window.addEventListener('resize', function() {
 *     calendar.refresh();
 * });
 */
Calendar.prototype.refresh = function() {
    if (this.refreshMethod) {
        this.refreshMethod();
    }

    this.render();
};

/**
 * Refresh child views
 * @param {string} [viewName] - the name of view to render. if not supplied then refresh all.
 * @private
 */
Calendar.prototype.refreshChildView = function(viewName) {
    if (!viewName) {
        this.render();

        return;
    }

    if (viewName === 'day') {
        viewName = 'week';
    }

    this.layout.children.items[viewName].render();
};

/**
 * Move to today.
 * @example
 * function onClickTodayBtn() {
 *     calendar.today();
 * }
 */
Calendar.prototype.today = function() {
    this.renderDate = new TZDate();

    this._setViewName(this.viewName); // see Calendar.move if (viewName === 'day') case using prevViewName 'week'se
    this.move();
    this.render();
};

/**
 * Move the calendar amount of offset value
 * @param {number} offset - offset value.
 * @private
 * @example
 * // move previous week when "week" view.
 * // move previous month when "month" view.
 * calendar.move(-1);
 */
Calendar.prototype.move = function(offset) {
    var renderDate = dw(this.renderDate),
        viewName = this.viewName,
        view = this.getCurrentView(),
        recursiveSet = this.setOptionRecurseively,
        startDate, endDate, tempDate,
        startDayOfWeek, visibleWeeksCount, workweek, datetimeOptions;

    offset = util.isExisty(offset) ? offset : 0;

    if (viewName === 'month') {
        startDayOfWeek = util.pick(this.options, 'month', 'startDayOfWeek') || 0;
        visibleWeeksCount = mmin(util.pick(this.options, 'month', 'visibleWeeksCount') || 0, 6);
        workweek = util.pick(this.options, 'month', 'workweek') || false;

        if (visibleWeeksCount) {
            datetimeOptions = {
                startDayOfWeek: startDayOfWeek,
                isAlways6Week: false,
                visibleWeeksCount: visibleWeeksCount,
                workweek: workweek
            };

            renderDate.addDate(offset * 7 * datetimeOptions.visibleWeeksCount);
            tempDate = datetime.arr2dCalendar(this.renderDate, datetimeOptions);

            recursiveSet(view, function(opt) {
                opt.renderMonth = datetime.format(renderDate.d, 'YYYY-MM-DD');
            });
        } else {
            datetimeOptions = {
                startDayOfWeek: startDayOfWeek,
                isAlways6Week: true,
                workweek: workweek
            };

            renderDate.addMonth(offset);
            tempDate = datetime.arr2dCalendar(this.renderDate, datetimeOptions);

            recursiveSet(view, function(opt) {
                opt.renderMonth = datetime.format(renderDate.d, 'YYYY-MM');
            });
        }

        startDate = tempDate[0][0];
        endDate = tempDate[tempDate.length - 1][tempDate[tempDate.length - 1].length - 1];
    } else if (viewName === 'week') {
        renderDate.addDate(offset * 7);
        startDayOfWeek = util.pick(this.options, 'week', 'startDayOfWeek') || 0;
        workweek = util.pick(this.options, 'week', 'workweek') || false;
        tempDate = this.getWeekDayRange(renderDate.d, startDayOfWeek, workweek);

        startDate = tempDate[0];
        endDate = tempDate[1];

        recursiveSet(view, function(opt) {
            opt.renderStartDate = datetime.format(startDate, 'YYYY-MM-DD');
            opt.renderEndDate = datetime.format(endDate, 'YYYY-MM-DD');
        });
    } else if (viewName === 'day') {
        renderDate.addDate(offset);
        startDate = endDate = renderDate.d;

        recursiveSet(view, function(opt) {
            opt.renderStartDate = datetime.format(startDate, 'YYYY-MM-DD');
            opt.renderEndDate = datetime.format(endDate, 'YYYY-MM-DD');
        });
    }

    this.renderDate = renderDate.d;
    this.renderRange = {
        start: startDate,
        end: endDate
    };
};

/**
 * Move to specific date
 * @param {(Date|string)} date - date to move
 * @example
 * calendar.on('clickDayname', function(event) {
 *     if (calendar.viewName === 'week') {
 *         calendar.setDate(new Date(event.date));
 *         calendar.toggleView('day', true);
 *     }
 * });
 */
Calendar.prototype.setDate = function(date) {
    if (util.isString(date)) {
        date = datetime.parse(date);
    }

    this.renderDate = new TZDate(Number(date));
    this._setViewName(this.viewName); // see Calendar.move if (viewName === 'day') case using prevViewName 'week'se
    this.move(0);
    this.render();
};

/**
 * Move the calendar forward a day, a week, a month
 * @example
 * function moveToNextOrPrevRange(val) {
    calendar.clear();
    if (val === -1) {
        calendar.prev();
    } else if (val === 1) {
        calendar.next();
    }
}
 */
Calendar.prototype.next = function() {
    this.move(1);
    this.render();
};

/**
 * Move the calendar backward a day, a week, a month
 * @example
 * function moveToNextOrPrevRange(val) {
    calendar.clear();
    if (val === -1) {
        calendar.prev();
    } else if (val === 1) {
        calendar.next();
    }
}
 */
Calendar.prototype.prev = function() {
    this.move(-1);
    this.render();
};

/**
 * Return current rendered view.
 * @returns {View} current view instance
 * @private
 */
Calendar.prototype.getCurrentView = function() {
    var viewName = this.viewName;

    if (viewName === 'day') {
        viewName = 'week';
    }

    return util.pick(this.layout.children.items, viewName);
};

/**
 * Change calendar's schedule color with option
 * @param {string} calendarId - calendar ID
 * @param {object} option - color data object
 *  @param {string} option.color - text color of schedule element
 *  @param {string} option.bgColor - bg color of schedule element
 *  @param {string} option.borderColor - border color of schedule element
 *  @param {boolean} [option.render=true] - set false then does not auto render.
 * @example
 * calendar.setCalendarColor('1', {
 *     color: '#e8e8e8',
 *     bgColor: '#585858',
 *     render: false
 * });
 * calendar.setCalendarColor('2', {
 *     color: '#282828',
 *     bgColor: '#dc9656',
 *     render: false
 * });
 * calendar.setCalendarColor('3', {
 *     color: '#a16946',
 *     bgColor: '#ab4642',
 *     render: true
 * });
 */
Calendar.prototype.setCalendarColor = function(calendarId, option) {
    var calColor = this.calendarColor,
        ownSchedules = this.controller.schedules,
        ownColor = calColor[calendarId];

    if (!util.isObject(option)) {
        config.throwError('Calendar#changeCalendarColor(): color 는 {color: \'\', bgColor: \'\'} 형태여야 합니다.');
    }

    ownColor = calColor[calendarId] = util.extend({
        color: '#000',
        bgColor: '#a1b56c',
        borderColor: '#a1b56c',
        render: true
    }, option);

    ownSchedules.each(function(model) {
        if (model.calendarId !== calendarId) {
            return;
        }

        model.color = ownColor.color;
        model.bgColor = ownColor.bgColor;
        model.borderColor = ownColor.borderColor;
    });

    if (ownColor.render) {
        this.render();
    }
};

/**
 * Show schedules visibility by calendar ID
 * @param {string|string[]} calendarId - calendar id value
 * @param {boolean} [render=true] - set false then doesn't render after change model's property.
 * @private
 */
Calendar.prototype.showSchedulesByCalendarID = function(calendarId, render) {
    render = util.isExisty(render) ? render : true;
    this._toggleSchedulesByCalendarID(calendarId, false, render);
};

/**
 * Hide schedules visibility by calendar ID
 * @param {string|string[]} calendarId - calendar id value
 * @param {boolean} [render=true] - set false then doesn't render after change model's property.
 * @private
 */
Calendar.prototype.hideSchedulesByCalendarID = function(calendarId, render) {
    render = util.isExisty(render) ? render : true;
    this._toggleSchedulesByCalendarID(calendarId, true, render);
};

/**********
 * Custom Events
 **********/

/**
 * 각 뷰의 클릭 핸들러와 사용자 클릭 이벤트 핸들러를 잇기 위한 브릿지 개념의 이벤트 핸들러
 * @fires Calendar#clickSchedule
 * @param {object} clickScheduleData - 'clickSchedule' 핸들러의 이벤트 데이터
 * @private
 */
Calendar.prototype._onClick = function(clickScheduleData) {
    /**
     * Fire this event when click a schedule.
     * @event Calendar#clickSchedule
     * @type {object}
     * @property {Schedule} schedule - schedule instance
     * @property {MouseEvent} event - MouseEvent
     * @example
     * calendar.on('clickSchedule', function(event) {
     *     var schedule = event.schedule;
     *
     *     if (lastClickSchedule) {
     *         calendar.updateSchedule(lastClickSchedule.id, lastClickSchedule.calendarId, {
     *             isFocused: false
     *         });
     *     }
     *     calendar.updateSchedule(schedule.id, schedule.calendarId, {
     *         isFocused: true
     *     });
     *
     *     lastClickSchedule = schedule;
     *     // open detail view
     * });
     */
    this.fire('clickSchedule', clickScheduleData);
};

/**
 * dayname 클릭 이벤트 핸들러
 * @fires Calendar#clickDayname
 * @param {object} clickScheduleData - 'clickDayname' 핸들러의 이벤트 데이터
 * @private
 */
Calendar.prototype._onClickDayname = function(clickScheduleData) {
    /**
     * Fire this event when click a day name in weekly.
     * @event Calendar#clickDayname
     * @type {object}
     * @property {string} date - date string by format 'YYYY-MM-DD'
     * @example
     * calendar.on('clickDayname', function(event) {
     *     if (calendar.viewName === 'week') {
     *         calendar.setDate(new Date(event.date));
     *         calendar.toggleView('day', true);
     *     }
     * });
     */
    this.fire('clickDayname', clickScheduleData);
};

/**
 * @fires {Calendar#beforeCreateSchedule}
 * @param {object} createScheduleData - select schedule data from allday, time
 * @private
 */
Calendar.prototype._onBeforeCreate = function(createScheduleData) {
    /**
     * Fire this event when select time period in daily, weekly, monthly.
     * @event Calendar#beforeCreateSchedule
     * @type {object}
     * @property {boolean} isAllDay - allday schedule
     * @property {Date} start - selected start time
     * @property {Date} end - selected end time
     * @property {TimeCreationGuide} guide - TimeCreationGuide instance
     * @property {string} triggerEventName - event name like 'click', 'dblclick'
     * @example
     * calendar.on('beforeCreateSchedule', function(event) {
     *     var startTime = event.start;
     *     var endTime = event.end;
     *     var isAllDay = event.isAllDay;
     *     var guide = event.guide;
     *     var triggerEventName = event.triggerEventName;
     *     var schedule;
     *
     *     if (triggerEventName === 'click') {
     *         // open writing simple schedule popup
     *         schedule = {...};
     *     } else if (triggerEventName === 'dblclick') {
     *         // open writing detail schedule popup
     *         schedule = {...};
     *     }
     *
     *     calendar.createSchedules([schedule]);
     * });
     */
    this.fire('beforeCreateSchedule', createScheduleData);
};

/**
 * @fires Calendar#beforeUpdateSchedule
 * @param {object} updateScheduleData - update schedule data
 * @private
 */
Calendar.prototype._onBeforeUpdate = function(updateScheduleData) {
    /**
     * Fire this event when drag a schedule to change time in daily, weekly, monthly.
     * @event Calendar#beforeUpdateSchedule
     * @type {object}
     * @property {Schedule} schedule - schedule instance to update
     * @property {Date} start - start time to update
     * @property {Date} end - end time to update
     * @example
     * calendar.on('beforeUpdateSchedule', function(event) {
     *     var schedule = event.schedule;
     *     var startTime = event.start;
     *     var endTime = event.end;
     *
     *     calendar.updateSchedule(schedule.id, schedule.calendarId, {
     *         start: startTime,
     *         end: endTime
     *     });
     * });
     */
    this.fire('beforeUpdateSchedule', updateScheduleData);
};

/**
 * @fires Calendar#resizePanel
 * @param {object} resizeScheduleData - resize schedule data object
 * @private
 */
Calendar.prototype._onResizePanel = function(resizeScheduleData) {
    /**
     * Fire this event when resize view panels(milestone, task, allday).
     * @event Calendar#resizePanel
     * @type {object}
     * @property {number[]} layoutData - layout data after resized
     * @example
     * calendar.on('resizePanel', function(layoutData) {
     *     console.log(layoutData);
     *     // do something to resize your UI if necessary.
     * });
     */
    this.fire('resizePanel', resizeScheduleData);
};

/**
 * 캘린더 팩토리 클래스와 주뷰, 월뷰의 이벤트 연결을 토글한다
 * @param {boolean} isAttach - true면 이벤트 연결함.
 * @param {Week|Month} view - 주뷰 또는 월뷰
 * @private
 */
Calendar.prototype._toggleViewSchedule = function(isAttach, view) {
    var self = this,
        handler = view.handler,
        isMonthView = view.viewName === 'month',
        method = isAttach ? 'on' : 'off';

    util.forEach(handler.click, function(clickHandler) {
        clickHandler[method]('clickSchedule', self._onClick, self);
    });

    util.forEach(handler.dayname, function(clickHandler) {
        clickHandler[method]('clickDayname', self._onClickDayname, self);
    });

    util.forEach(handler.creation, function(creationHandler) {
        creationHandler[method]('beforeCreateSchedule', self._onBeforeCreate, self);
    });

    util.forEach(handler.move, function(moveHandler) {
        moveHandler[method]('beforeUpdateSchedule', self._onBeforeUpdate, self);
    });

    util.forEach(handler.resize, function(resizeHandler) {
        resizeHandler[method]('beforeUpdateSchedule', self._onBeforeUpdate, self);
    });

    if (!isMonthView) {
        view.vLayout[method]('resize', self._onResizePanel, self);
    }
};

/**
 * Toggle current view
 * @param {string} newViewName - new view name to render
 * @param {boolean} force - force render despite of current view and new view are equal
 * @example
 * // daily view
 * calendar.toggleView('day', true);
 *
 * // weekly view
 * calendar.toggleView('week', true);
 *
 * // monthly view(default 6 weeks view)
 * calendar.options.month.visibleWeeksCount = 6; // or null
 * calendar.toggleView('month', true);
 *
 * // 2 weeks monthly view
 * calendar.options.month.visibleWeeksCount = 2;
 * calendar.toggleView('month', true);
 *
 * // 3 weeks monthly view
 * calendar.options.month.visibleWeeksCount = 3;
 * calendar.toggleView('month', true);
 *
 * // narrow weekend
 * calendar.options.month.narrowWeekend = true;
 * calendar.options.week.narrowWeekend = true;
 * calendar.toggleView(calendar.viewName, true);
 *
 * // change start day of week(from monday)
 * calendar.options.month.startDayOfWeek = 1;
 * calendar.options.week.startDayOfWeek = 1;
 * calendar.toggleView(calendar.viewName, true);
 */
Calendar.prototype.toggleView = function(newViewName, force) {
    var self = this,
        layout = this.layout,
        controller = this.controller,
        dragHandler = this.dragHandler,
        options = this.options,
        viewName = this.viewName,
        created;

    if (!force && viewName === newViewName) {
        return;
    }

    this._setViewName(newViewName);

    // convert day to week
    if (viewName === 'day') {
        viewName = 'week';
    }

    if (newViewName === 'day') {
        newViewName = 'week';
    }
    layout.children.doWhenHas(viewName, function(view) {
        self._toggleViewSchedule(false, view);
    });

    layout.clear();

    if (newViewName === 'month') {
        created = this.createMonthView(
            controller,
            layout.container,
            dragHandler,
            options
        );
    } else if (newViewName === 'week' || newViewName === 'day') {
        created = this.createWeekView(
            controller,
            layout.container,
            dragHandler,
            options
        );
    }

    layout.addChild(created.view);

    layout.children.doWhenHas(newViewName, function(view) {
        self._toggleViewSchedule(true, view);
    });

    this.refreshMethod = created.refresh;
    this.scrollToNowMethod = created.scrollToNow;

    this.move();
    this.render();
};

/**
 * Toggle task view('Milestone', 'Task') panel
 * @param {boolean} enabled - use task view
 * @example
 * // There is no milestone, task, so hide those view panel
 * calendar.toggleTaskView(false);
 *
 * // There are some milestone, task, so show those view panel.
 * calendar.toggleTaskView(true);
 */
Calendar.prototype.toggleTaskView = function(enabled) {
    var viewName = this.viewName,
        options = this.options;

    options.taskView = enabled;

    this.toggleView(viewName, true);
};

/**
 * Toggle schedule view('AllDay', TimeGrid') panel
 * @param {boolean} enabled - use task view
 * @example
 * // hide those view panel to show only 'Milestone', 'Task'
 * calendar.toggleScheduleView(false);
 *
 * // show those view panel.
 * calendar.toggleScheduleView(true);
 */
Calendar.prototype.toggleScheduleView = function(enabled) {
    var viewName = this.viewName,
        options = this.options;

    options.scheduleView = enabled;

    this.toggleView(viewName, true);
};

/**
 * Set current view name
 * @param {string} viewName - new view name to render
 * @private
 */
Calendar.prototype._setViewName = function(viewName) {
    this.prevViewName = this.viewName;
    this.viewName = viewName;
};

/**
 * Get schedule by schedule id and calendar id.
 * @param {string} scheduleId - ID of schedule
 * @param {string} calendarId - calendarId of schedule
 * @returns {HTMLElement} schedule element if found or null
 * @example
 * var element = calendar.getElement(scheduleId, calendarId);
 * console.log(element);
 */
Calendar.prototype.getElement = function(scheduleId, calendarId) {
    var schedule = this.getSchedule(scheduleId, calendarId);
    if (schedule) {
        return document.querySelector('[data-schedule-id="' + scheduleId + '"][data-calendar-id="' + calendarId + '"]');
    }

    return null;
};

/**
 * Set timezone offset
 * @param {number} offset - offset (min)
 * @static
 * @example
 * var timezoneName = moment.tz.guess();
 * tui.Calendar.setTimezoneOffset(moment.tz.zone(timezoneName).utcOffset(moment()));
 */
Calendar.setTimezoneOffset = function(offset) {
    timezone.setOffset(offset);
};

/**
 * Set a callback function to get timezone offset by timestamp
 * @param {function} callback - callback function
 * @static
 * @example
 * var timezoneName = moment.tz.guess();
 * tui.Calendar.setTimezoneOffsetCallback(function(timestamp) {
 *      return moment.tz.zone(timezoneName).utcOffset(timestamp));
 * });
 */
Calendar.setTimezoneOffsetCallback = function(callback) {
    timezone.setOffsetCallback(callback);
};

util.CustomEvents.mixin(Calendar);

module.exports = Calendar;