/**
 * @fileoverview View for rendered schedules by times.
 * @author NHN FE Development Lab <dl_javascript@nhn.com>
 */
'use strict';

var util = require('tui-code-snippet');
var config = require('../../config');
var common = require('../../common/common');
var domutil = require('../../common/domutil');
var domevent = require('../../common/domevent');
var datetime = require('../../common/datetime');
var tz = require('../../common/timezone');
var reqAnimFrame = require('../../common/reqAnimFrame');
var View = require('../view');
var Time = require('./time');
var AutoScroll = require('../../common/autoScroll');
var mainTmpl = require('../template/week/timeGrid.hbs');
var timezoneStickyTmpl = require('../template/week/timezoneSticky.hbs');
var timegridCurrentTimeTmpl = require('../template/week/timeGridCurrentTime.hbs');
var TZDate = tz.Date;
var HOURMARKER_REFRESH_INTERVAL = 1000 * 60;
var SIXTY_SECONDS = 60;
var SIXTY_MINUTES = 60;

/**
 * Returns a list of time labels from start to end.
 * For hidden labels near the current time, set to hidden: true.
 * @param {object} opt - TimeGrid.options
 * @param {boolean} hasHourMarker - Whether the current time is displayed
 * @param {number} timezoneOffset - timezone offset
 * @param {object} styles - styles
 * @returns {Array.<Object>}
 */
function getHoursLabels(opt, hasHourMarker, timezoneOffset, styles) {
    var hourStart = opt.hourStart;
    var hourEnd = opt.hourEnd;
    var renderEndDate = new TZDate(opt.renderEndDate);
    var shiftByOffset = parseInt(timezoneOffset / SIXTY_MINUTES, 10);
    var shiftMinutes = Math.abs(timezoneOffset % SIXTY_MINUTES);
    var now = new TZDate().toLocalTime();
    var nowMinutes = now.getMinutes();
    var hoursRange = util.range(0, 24);
    var nowAroundHours = null;
    var nowHours, nowHoursIndex;
    var isNegativeZero = 1 / -Infinity === shiftByOffset;

    if ((shiftByOffset < 0 || isNegativeZero) && shiftMinutes > 0) {
        shiftByOffset -= 1;
    }

    // shift the array and take elements between start and end
    common.shiftArray(hoursRange, shiftByOffset);
    common.takeArray(hoursRange, hourStart, hourEnd);

    nowHours = common.shiftHours(now.getHours(), shiftByOffset) % 24;
    nowHoursIndex = util.inArray(nowHours, hoursRange);

    if (hasHourMarker) {
        if (nowMinutes < 20) {
            nowAroundHours = nowHours;
        } else if (nowMinutes > 40) {
            nowAroundHours = nowHours + 1;
        }

        if (util.isNumber(nowAroundHours)) {
            nowAroundHours %= 24;
        }
    }

    return util.map(hoursRange, function(hour, index) {
        var color;
        var fontWeight;
        var isPast =
            (hasHourMarker && index <= nowHoursIndex) ||
            (renderEndDate < now && !datetime.isSameDate(renderEndDate, now));

        if (isPast) {
            // past
            color = styles.pastTimeColor;
            fontWeight = styles.pastTimeFontWeight;
        } else {
            // future
            color = styles.futureTimeColor;
            fontWeight = styles.futureTimeFontWeight;
        }

        return {
            hour: hour,
            minutes: shiftMinutes,
            hidden: nowAroundHours === hour || index === 0,
            color: color || '',
            fontWeight: fontWeight || ''
        };
    });
}

/**
 * Returns timezone offset from timezone object
 * @param {object} timezoneObj - timezone object in options.timzones
 * @param {number} timestamp - timestamp
 * @returns {number} timezoneOffset - timezone offset
 */
function getOffsetByTimezoneOption(timezoneObj, timestamp) {
    var primaryOffset = tz.getPrimaryOffset();
    if (util.isString(timezoneObj.timezoneName)) {
        return -tz.getOffsetByTimezoneName(timezoneObj.timezoneName, timestamp);
    }

    // @deprecated timezoneOffset property will be deprecated
    if (util.isNumber(timezoneObj.timezoneOffset) && timezoneObj.timezoneOffset !== primaryOffset) {
        return timezoneObj.timezoneOffset;
    }

    return -primaryOffset;
}

/**
 * @constructor
 * @extends {View}
 * @param {string} name - view name
 * @param {object} options The object for view customization.
 * @param {string} options.renderStartDate - render start date. YYYY-MM-DD
 * @param {string} options.renderEndDate - render end date. YYYY-MM-DD
 * @param {number} [options.hourStart=0] You can change view's start hours.
 * @param {number} [options.hourEnd=0] You can change view's end hours.
 * @param {HTMLElement} panelElement panel element.
 */
function TimeGrid(name, options, panelElement) {
    var container = domutil.appendHTMLElement(
        'div',
        panelElement,
        config.classname('timegrid-container')
    );
    var stickyContainer = domutil.appendHTMLElement(
        'div',
        panelElement,
        config.classname('timegrid-sticky-container')
    );

    panelElement.style.position = 'relative'; // for stickyContainer

    name = name || 'time';

    View.call(this, container);

    if (!util.browser.safari) {
        /**
         * @type {AutoScroll}
         */
        this._autoScroll = new AutoScroll(container);
    }

    this.stickyContainer = stickyContainer;

    /**
     * Time view options.
     * @type {object}
     */
    this.options = util.extend(
        {
            viewName: name,
            renderStartDate: '',
            renderEndDate: '',
            hourStart: 0,
            hourEnd: 24,
            timezones: options.timezones,
            isReadOnly: options.isReadOnly,
            showTimezoneCollapseButton: false
        },
        options.week
    );

    if (this.options.timezones.length < 1) {
        this.options.timezones = [
            {
                timezoneOffset: tz.getPrimaryOffset()
            }
        ];
    }

    /**
     * Interval id for hourmarker animation.
     * @type {number}
     */
    this.intervalID = 0;

    /**
     * timer id for hourmarker initial state
     * @type {number}
     */
    this.timerID = 0;

    /**
     * requestAnimationFrame unique ID
     * @type {number}
     */
    this.rAnimationFrameID = 0;

    /**
     * @type {boolean}
     */
    this._scrolled = false;

    /**
     * cache parent's view model
     * @type {object}
     */
    this._cacheParentViewModel = null;

    /**
     * cache hoursLabels view model to render again TimeGrid
     * @type {object}
     */
    this._cacheHoursLabels = null;

    this.attachEvent();
}

util.inherit(TimeGrid, View);

/**********
 * Prototype props
 **********/

/**
 * @type {string}
 */
TimeGrid.prototype.viewName = 'timegrid';

/**
 * Destroy view.
 * @override
 */
TimeGrid.prototype._beforeDestroy = function() {
    clearInterval(this.intervalID);
    clearTimeout(this.timerID);
    reqAnimFrame.cancelAnimFrame(this.rAnimationFrameID);

    if (this._autoScroll) {
        this._autoScroll.destroy();
    }

    domevent.off(this.stickyContainer, 'click', this._onClickStickyContainer, this);

    this._autoScroll = this.hourmarkers = this.intervalID
        = this.timerID = this.rAnimationFrameID = this._cacheParentViewModel = this.stickyContainer = null;
};

/**
 * @param {Date} [time] - date object to convert pixel in grids.
 * use **Date.now()** when not supplied.
 * @returns {number} The pixel value represent current time in grids.
 */
TimeGrid.prototype._getTopPercentByTime = function(time) {
    var opt = this.options,
        raw = datetime.raw(time || new TZDate()),
        hourLength = util.range(opt.hourStart, opt.hourEnd).length,
        maxMilliseconds = hourLength * datetime.MILLISECONDS_PER_HOUR,
        hmsMilliseconds =
            datetime.millisecondsFrom('hour', raw.h) +
            datetime.millisecondsFrom('minutes', raw.m) +
            datetime.millisecondsFrom('seconds', raw.s) +
            raw.ms,
        topPercent;

    topPercent = common.ratio(maxMilliseconds, 100, hmsMilliseconds);
    topPercent -= common.ratio(
        maxMilliseconds,
        100,
        datetime.millisecondsFrom('hour', opt.hourStart)
    );

    return common.limit(topPercent, [0], [100]);
};

/**
 * Get Hourmarker viewmodel.
 * @param {TZDate} now - now
 * @param {object} grids grid information(width, left, day)
 * @param {Array.<TZDate>} range render range
 * @returns {object} ViewModel of hourmarker.
 */
TimeGrid.prototype._getHourmarkerViewModel = function(now, grids, range) {
    var todaymarkerLeft = -1;
    var todaymarkerWidth = -1;
    var hourmarkerTimzones = [];
    var opt = this.options;
    var primaryOffset = tz.getPrimaryOffset();
    var timezones = opt.timezones;
    var viewModel;

    util.forEach(range, function(date, index) {
        if (datetime.isSameDate(now, date)) {
            todaymarkerLeft = grids[index] ? grids[index].left : 0;
            todaymarkerWidth = grids[index] ? grids[index].width : 0;
        }
    });

    util.forEach(timezones, function(timezone) {
        var hourmarker = new TZDate(now);
        var timezoneOffset = getOffsetByTimezoneOption(timezone, hourmarker.getTime());
        var timezoneDifference = timezoneOffset + primaryOffset;
        var dateDifference;

        hourmarker.setMinutes(hourmarker.getMinutes() + timezoneDifference);
        dateDifference = datetime.getDateDifference(hourmarker, now);

        hourmarkerTimzones.push({
            hourmarker: hourmarker,
            dateDifferenceSign: dateDifference < 0 ? '-' : '+',
            dateDifference: Math.abs(dateDifference)
        });
    });

    viewModel = {
        currentHours: now.getHours(),
        hourmarkerTop: this._getTopPercentByTime(now),
        hourmarkerTimzones: hourmarkerTimzones,
        todaymarkerLeft: todaymarkerLeft,
        todaymarkerWidth: todaymarkerWidth,
        todaymarkerRight: todaymarkerLeft + todaymarkerWidth
    };

    return viewModel;
};

/**
 * Get timezone view model
 * @param {number} currentHours - current hour
 * @param {boolean} timezonesCollapsed - multiple timezones are collapsed.
 * @param {object} styles - styles
 * @returns {object} ViewModel
 */
TimeGrid.prototype._getTimezoneViewModel = function(currentHours, timezonesCollapsed, styles) {
    var opt = this.options;
    var primaryOffset = tz.getPrimaryOffset();
    var timezones = opt.timezones;
    var timezonesLength = timezones.length;
    var timezoneViewModel = [];
    var collapsed = timezonesCollapsed;
    var width = collapsed ? 100 : 100 / timezonesLength;
    var now = new TZDate().toLocalTime();
    var backgroundColor = styles.displayTimezoneLabelBackgroundColor;

    // eslint-disable-next-line complexity
    util.forEach(timezones, function(timezone, index) {
        var hourmarker = new TZDate(now);
        var timezoneOffset = getOffsetByTimezoneOption(timezone, hourmarker.getTime());
        var timezoneDifference = timezoneOffset + primaryOffset;
        var timeSlots = getHoursLabels(opt, currentHours >= 0, timezoneDifference, styles);
        var dateDifference;

        hourmarker.setMinutes(hourmarker.getMinutes() + timezoneDifference);
        dateDifference = datetime.getDateDifference(hourmarker, now);

        if (index > 0) {
            backgroundColor = styles.additionalTimezoneBackgroundColor;
        }

        timezoneViewModel.push({
            timeSlots: timeSlots,
            displayLabel: timezone.displayLabel,
            timezoneOffset: timezone.timezoneOffset,
            tooltip: timezone.tooltip || '',
            width: width,
            left: collapsed ? 0 : (timezones.length - index - 1) * width,
            isPrimary: index === 0,
            backgroundColor: backgroundColor || '',
            hidden: index !== 0 && collapsed,
            hourmarker: hourmarker,
            dateDifferenceSign: dateDifference < 0 ? '-' : '+',
            dateDifference: Math.abs(dateDifference)
        });
    });

    return timezoneViewModel;
};

/**
 * Get base viewModel.
 * @param {object} viewModel - view model
 * @returns {object} ViewModel
 */
TimeGrid.prototype._getBaseViewModel = function(viewModel) {
    var grids = viewModel.grids;
    var range = viewModel.range;
    var opt = this.options;
    var baseViewModel = this._getHourmarkerViewModel(new TZDate().toLocalTime(), grids, range);
    var timezonesCollapsed = util.pick(viewModel, 'state', 'timezonesCollapsed');
    var styles = this._getStyles(viewModel.theme, timezonesCollapsed);

    return util.extend(baseViewModel, {
        timezones: this._getTimezoneViewModel(
            baseViewModel.todaymarkerLeft,
            timezonesCollapsed,
            styles
        ),
        hoursLabels: getHoursLabels(opt, baseViewModel.todaymarkerLeft >= 0, 0, styles),
        styles: styles,
        showTimezoneCollapseButton: util.pick(opt, 'showTimezoneCollapseButton'),
        timezonesCollapsed: timezonesCollapsed
    });
};

/**
 * Reconcilation child views and render.
 * @param {object} viewModels Viewmodel
 * @param {object} grids grid information(width, left, day)
 * @param {HTMLElement} container Container element for each time view.
 * @param {Theme} theme - theme instance
 */
TimeGrid.prototype._renderChildren = function(viewModels, grids, container, theme) {
    var self = this,
        options = this.options,
        childOption,
        child,
        isToday,
        containerHeight,
        today = datetime.format(new TZDate().toLocalTime(), 'YYYYMMDD'),
        i = 0;

    // clear contents
    container.innerHTML = '';
    this.children.clear();

    containerHeight = domutil.getSize(container.parentElement)[1];

    // reconcilation of child views
    util.forEach(viewModels, function(schedules, ymd) {
        isToday = ymd === today;

        childOption = {
            index: i,
            left: grids[i] ? grids[i].left : 0,
            width: grids[i] ? grids[i].width : 0,
            ymd: ymd,
            isToday: isToday,
            isPending: options.isPending,
            isFocused: options.isFocused,
            isReadOnly: options.isReadOnly,
            hourStart: options.hourStart,
            hourEnd: options.hourEnd
        };

        child = new Time(
            childOption,
            domutil.appendHTMLElement('div', container, config.classname('time-date')),
            theme
        );
        child.render(ymd, schedules, containerHeight);

        self.addChild(child);

        i += 1;
    });
};

/**
 * @override
 * @param {object} viewModel ViewModel list from Week view.
 */
TimeGrid.prototype.render = function(viewModel) {
    var opt = this.options,
        timeViewModel = viewModel.schedulesInDateRange[opt.viewName],
        container = this.container,
        grids = viewModel.grids,
        baseViewModel = this._getBaseViewModel(viewModel),
        scheduleLen = util.keys(timeViewModel).length;

    this._cacheParentViewModel = viewModel;
    this._cacheHoursLabels = baseViewModel.hoursLabels;

    if (!scheduleLen) {
        return;
    }

    baseViewModel.showHourMarker = baseViewModel.todaymarkerLeft >= 0;

    container.innerHTML = mainTmpl(baseViewModel);

    /**********
     * Render sticky container for timezone display label
     **********/
    this.renderStickyContainer(baseViewModel);

    /**********
     * Render children
     **********/
    this._renderChildren(
        timeViewModel,
        grids,
        domutil.find(config.classname('.timegrid-schedules-container'), container),
        viewModel.theme
    );

    this._hourLabels = domutil.find('ul', container);

    /**********
     * Render hourmarker
     **********/
    this.hourmarkers = domutil.find(config.classname('.timegrid-hourmarker'), container, true);

    if (!this._scrolled) {
        this._scrolled = true;
        this.scrollToNow();
    }
};

TimeGrid.prototype.renderStickyContainer = function(baseViewModel) {
    var stickyContainer = this.stickyContainer;

    stickyContainer.innerHTML = timezoneStickyTmpl(baseViewModel);

    stickyContainer.style.display = baseViewModel.timezones.length > 1 ? 'block' : 'none';
    stickyContainer.style.width = baseViewModel.styles.leftWidth;
    stickyContainer.style.height = baseViewModel.styles.displayTimezoneLabelHeight;
    stickyContainer.style.borderBottom = baseViewModel.styles.leftBorderRight;
};

/**
 * Refresh hourmarker element.
 */
TimeGrid.prototype.refreshHourmarker = function() {
    var hourmarkers = this.hourmarkers;
    var viewModel = this._cacheParentViewModel;
    var hoursLabels = this._cacheHoursLabels;
    var rAnimationFrameID = this.rAnimationFrameID;
    var baseViewModel;

    if (!hourmarkers || !viewModel || rAnimationFrameID) {
        return;
    }

    baseViewModel = this._getBaseViewModel(viewModel);

    this.rAnimationFrameID = reqAnimFrame.requestAnimFrame(function() {
        var needsRender = false;

        util.forEach(hoursLabels, function(hoursLabel, index) {
            if (hoursLabel.hidden !== baseViewModel.hoursLabels[index].hidden) {
                needsRender = true;

                return false;
            }

            return true;
        });

        if (needsRender) {
            this.render(viewModel);
        } else {
            util.forEach(hourmarkers, function(hourmarker) {
                var todaymarker = domutil.find(
                    config.classname('.timegrid-todaymarker'),
                    hourmarker
                );
                var hourmarkerContainer = domutil.find(
                    config.classname('.timegrid-hourmarker-time'),
                    hourmarker
                );
                var timezone = domutil.closest(hourmarker, config.classname('.timegrid-timezone'));
                var timezoneIndex = timezone ? domutil.getData(timezone, 'timezoneIndex') : 0;

                hourmarker.style.top = baseViewModel.hourmarkerTop + '%';

                if (todaymarker) {
                    todaymarker.style.display =
                        baseViewModel.todaymarkerLeft >= 0 ? 'block' : 'none';
                }
                if (hourmarkerContainer) {
                    hourmarkerContainer.innerHTML = timegridCurrentTimeTmpl(
                        baseViewModel.hourmarkerTimzones[timezoneIndex]
                    );
                }
            });
        }

        this.rAnimationFrameID = null;
    }, this);
};

/**
 * Attach events
 */
TimeGrid.prototype.attachEvent = function() {
    clearInterval(this.intervalID);
    clearTimeout(this.timerID);
    this.intervalID = this.timerID = this.rAnimationFrameID = null;

    this.timerID = setTimeout(
        this.onTick.bind(this),
        (SIXTY_SECONDS - new TZDate().getSeconds()) * 1000
    );

    domevent.on(this.stickyContainer, 'click', this._onClickStickyContainer, this);
};

/**
 * Scroll time grid to current hourmarker.
 */
TimeGrid.prototype.scrollToNow = function() {
    var container = this.container;
    var offsetTop, viewBound, scrollTop, scrollAmount, scrollBy, scrollFn;

    if (!this.hourmarkers || !this.hourmarkers.length) {
        return;
    }

    offsetTop = this.hourmarkers[0].offsetTop;
    viewBound = this.getViewBound();
    scrollTop = offsetTop;
    scrollAmount = viewBound.height / 4;
    scrollBy = 10;

    scrollFn = function() {
        if (scrollTop > offsetTop - scrollAmount) {
            scrollTop -= scrollBy;
            container.scrollTop = scrollTop;

            reqAnimFrame.requestAnimFrame(scrollFn);
        } else {
            container.scrollTop = offsetTop - scrollAmount;
        }
    };

    reqAnimFrame.requestAnimFrame(scrollFn);
};

/**********
 * Schedule handlers
 **********/

/**
 * Interval tick handler
 */
TimeGrid.prototype.onTick = function() {
    if (this.timerID) {
        clearTimeout(this.timerID);
        this.timerID = null;
    }

    if (!this.intervalID) {
        this.intervalID = setInterval(this.onTick.bind(this), HOURMARKER_REFRESH_INTERVAL);
    }
    this.refreshHourmarker();
};

/**
 * Get the styles from theme
 * @param {Theme} theme - theme instance
 * @param {boolean} timezonesCollapsed - multiple timezones are collapsed.
 * @returns {object} styles - styles object
 */
// eslint-disable-next-line complexity
TimeGrid.prototype._getStyles = function(theme, timezonesCollapsed) {
    var styles = {};
    var timezonesLength = this.options.timezones.length;
    var collapsed = timezonesCollapsed;
    var numberAndUnit;

    if (theme) {
        styles.borderBottom = theme.week.timegridHorizontalLine.borderBottom || theme.common.border;
        styles.halfHourBorderBottom =
            theme.week.timegridHalfHour.borderBottom || theme.common.border;

        styles.todayBackgroundColor = theme.week.today.backgroundColor;
        styles.weekendBackgroundColor = theme.week.weekend.backgroundColor;
        styles.backgroundColor = theme.week.daygrid.backgroundColor;
        styles.leftWidth = theme.week.timegridLeft.width;
        styles.leftBackgroundColor = theme.week.timegridLeft.backgroundColor;
        styles.leftBorderRight = theme.week.timegridLeft.borderRight || theme.common.border;
        styles.leftFontSize = theme.week.timegridLeft.fontSize;
        styles.timezoneWidth = theme.week.timegridLeft.width;
        styles.additionalTimezoneBackgroundColor =
            theme.week.timegridLeftAdditionalTimezone.backgroundColor || styles.leftBackgroundColor;

        styles.displayTimezoneLabelHeight = theme.week.timegridLeftTimezoneLabel.height;
        styles.displayTimezoneLabelBackgroundColor =
            theme.week.timegridLeft.backgroundColor === 'inherit'
                ? 'white'
                : theme.week.timegridLeft.backgroundColor;

        styles.oneHourHeight = theme.week.timegridOneHour.height;
        styles.halfHourHeight = theme.week.timegridHalfHour.height;
        styles.quaterHourHeight = (parseInt(styles.halfHourHeight, 10) / 2) + 'px';

        styles.currentTimeColor = theme.week.currentTime.color;
        styles.currentTimeFontSize = theme.week.currentTime.fontSize;
        styles.currentTimeFontWeight = theme.week.currentTime.fontWeight;

        styles.pastTimeColor = theme.week.pastTime.color;
        styles.pastTimeFontWeight = theme.week.pastTime.fontWeight;

        styles.futureTimeColor = theme.week.futureTime.color;
        styles.futureTimeFontWeight = theme.week.futureTime.fontWeight;

        styles.currentTimeLeftBorderTop = theme.week.currentTimeLinePast.border;
        styles.currentTimeBulletBackgroundColor = theme.week.currentTimeLineBullet.backgroundColor;
        styles.currentTimeTodayBorderTop = theme.week.currentTimeLineToday.border;
        styles.currentTimeRightBorderTop = theme.week.currentTimeLineFuture.border;

        if (!collapsed && timezonesLength > 1) {
            numberAndUnit = common.parseUnit(styles.leftWidth);
            styles.leftWidth = (numberAndUnit[0] * timezonesLength) + numberAndUnit[1];
        }
    }

    return styles;
};

/**
 * @param {MouseEvent} event - mouse event object
 */
TimeGrid.prototype._onClickStickyContainer = function(event) {
    var target = domevent.getEventTarget(event);
    var closeBtn = domutil.closest(target, config.classname('.timegrid-timezone-close-btn'));

    if (!closeBtn) {
        return;
    }

    this.fire('clickTimezonesCollapsedBtn');
};

module.exports = TimeGrid;
