"use strict";
/*
 * Copyright (C) 2012 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


/**
 * @enum {number}
 */
var WeekDay = {
    Sunday: 0,
    Monday: 1,
    Tuesday: 2,
    Wednesday: 3,
    Thursday: 4,
    Friday: 5,
    Saturday: 6
};

/**
 * @type {Object}
 */
var global = {
    picker: null,
    params: {
        locale: "en_US",
        weekStartDay: WeekDay.Sunday,
        dayLabels: ["S", "M", "T", "W", "T", "F", "S"],
        shortMonthLabels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"],
        isLocaleRTL: false,
        mode: "date",
        weekLabel: "Week",
        anchorRectInScreen: new Rectangle(0, 0, 0, 0),
        currentValue: null
    }
};

// ----------------------------------------------------------------
// Utility functions

/**
 * @return {!bool}
 */
function hasInaccuratePointingDevice() {
    return matchMedia("(pointer: coarse)").matches;
}

/**
 * @return {!string} lowercase locale name. e.g. "en-us"
 */
function getLocale() {
    return (global.params.locale || "en-us").toLowerCase();
}

/**
 * @return {!string} lowercase language code. e.g. "en"
 */
function getLanguage() {
    var locale = getLocale();
    var result = locale.match(/^([a-z]+)/);
    if (!result)
        return "en";
    return result[1];
}

/**
 * @param {!number} number
 * @return {!string}
 */
function localizeNumber(number) {
    return window.pagePopupController.localizeNumberString(number);
}

/**
 * @const
 * @type {number}
 */
var ImperialEraLimit = 2087;

/**
 * @param {!number} year
 * @param {!number} month
 * @return {!string}
 */
function formatJapaneseImperialEra(year, month) {
    // We don't show an imperial era if it is greater than 99 becase of space
    // limitation.
    if (year > ImperialEraLimit)
        return "";
    if (year > 1989)
        return "(平成" + localizeNumber(year - 1988) + "年)";
    if (year == 1989)
        return "(平成元年)";
    if (year >= 1927)
        return "(昭和" + localizeNumber(year - 1925) + "年)";
    if (year > 1912)
        return "(大正" + localizeNumber(year - 1911) + "年)";
    if (year == 1912 && month >= 7)
        return "(大正元年)";
    if (year > 1868)
        return "(明治" + localizeNumber(year - 1867) + "年)";
    if (year == 1868)
        return "(明治元年)";
    return "";
}

function createUTCDate(year, month, date) {
    var newDate = new Date(0);
    newDate.setUTCFullYear(year);
    newDate.setUTCMonth(month);
    newDate.setUTCDate(date);
    return newDate;
}

/**
 * @param {string} dateString
 * @return {?Day|Week|Month}
 */
function parseDateString(dateString) {
    var month = Month.parse(dateString);
    if (month)
        return month;
    var week = Week.parse(dateString);
    if (week)
        return week;
    return Day.parse(dateString);
}

/**
 * @const
 * @type {number}
 */
var DaysPerWeek = 7;

/**
 * @const
 * @type {number}
 */
var MonthsPerYear = 12;

/**
 * @const
 * @type {number}
 */
var MillisecondsPerDay = 24 * 60 * 60 * 1000;

/**
 * @const
 * @type {number}
 */
var MillisecondsPerWeek = DaysPerWeek * MillisecondsPerDay;

/**
 * @constructor
 */
function DateType() {
}

/**
 * @constructor
 * @extends DateType
 * @param {!number} year
 * @param {!number} month
 * @param {!number} date
 */
function Day(year, month, date) {
    var dateObject = createUTCDate(year, month, date);
    if (isNaN(dateObject.valueOf()))
        throw "Invalid date";
    /**
     * @type {number}
     * @const
     */
    this.year = dateObject.getUTCFullYear();   
     /**
     * @type {number}
     * @const
     */  
    this.month = dateObject.getUTCMonth();
    /**
     * @type {number}
     * @const
     */
    this.date = dateObject.getUTCDate();
};

Day.prototype = Object.create(DateType.prototype);

Day.ISOStringRegExp = /^(\d+)-(\d+)-(\d+)/;

/**
 * @param {!string} str
 * @return {?Day}
 */
Day.parse = function(str) {
    var match = Day.ISOStringRegExp.exec(str);
    if (!match)
        return null;
    var year = parseInt(match[1], 10);
    var month = parseInt(match[2], 10) - 1;
    var date = parseInt(match[3], 10);
    return new Day(year, month, date);
};

/**
 * @param {!number} value
 * @return {!Day}
 */
Day.createFromValue = function(millisecondsSinceEpoch) {
    return Day.createFromDate(new Date(millisecondsSinceEpoch))
};

/**
 * @param {!Date} date
 * @return {!Day}
 */
Day.createFromDate = function(date) {
    if (isNaN(date.valueOf()))
        throw "Invalid date";
    return new Day(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
};

/**
 * @param {!Day} day
 * @return {!Day}
 */
Day.createFromDay = function(day) {
    return day;
};

/**
 * @return {!Day}
 */
Day.createFromToday = function() {
    var now = new Date();
    return new Day(now.getFullYear(), now.getMonth(), now.getDate());
};

/**
 * @param {!DateType} other
 * @return {!boolean}
 */
Day.prototype.equals = function(other) {
    return other instanceof Day && this.year === other.year && this.month === other.month && this.date === other.date;
};

/**
 * @param {!number=} offset
 * @return {!Day}
 */
Day.prototype.previous = function(offset) {
    if (typeof offset === "undefined")
        offset = 1;
    return new Day(this.year, this.month, this.date - offset);
};

/**
 * @param {!number=} offset
 * @return {!Day}
 */
Day.prototype.next = function(offset) {
 if (typeof offset === "undefined")
     offset = 1;
    return new Day(this.year, this.month, this.date + offset);
};

/**
 * @return {!Date}
 */
Day.prototype.startDate = function() {
    return createUTCDate(this.year, this.month, this.date);
};

/**
 * @return {!Date}
 */
Day.prototype.endDate = function() {
    return createUTCDate(this.year, this.month, this.date + 1);
};

/**
 * @return {!Day}
 */
Day.prototype.firstDay = function() {
    return this;
};

/**
 * @return {!Day}
 */
Day.prototype.middleDay = function() {
    return this;
};

/**
 * @return {!Day}
 */
Day.prototype.lastDay = function() {
    return this;
};

/**
 * @return {!number}
 */
Day.prototype.valueOf = function() {
    return createUTCDate(this.year, this.month, this.date).getTime();
};

/**
 * @return {!WeekDay}
 */
Day.prototype.weekDay = function() {
    return createUTCDate(this.year, this.month, this.date).getUTCDay();
};

/**
 * @return {!string}
 */
Day.prototype.toString = function() {
    var yearString = String(this.year);
    if (yearString.length < 4)
        yearString = ("000" + yearString).substr(-4, 4);
    return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2) + "-" + ("0" + this.date).substr(-2, 2);
};

// See WebCore/platform/DateComponents.h.
Day.Minimum = Day.createFromValue(-62135596800000.0);
Day.Maximum = Day.createFromValue(8640000000000000.0);

// See WebCore/html/DayInputType.cpp.
Day.DefaultStep = 86400000;
Day.DefaultStepBase = 0;

/**
 * @constructor
 * @extends DateType
 * @param {!number} year
 * @param {!number} week
 */
function Week(year, week) { 
    /**
     * @type {number}
     * @const
     */
    this.year = year;
    /**
     * @type {number}
     * @const
     */
    this.week = week;
    // Number of years per year is either 52 or 53.
    if (this.week < 1 || (this.week > 52 && this.week > Week.numberOfWeeksInYear(this.year))) {
        var normalizedWeek = Week.createFromDay(this.firstDay());
        this.year = normalizedWeek.year;
        this.week = normalizedWeek.week;
    }
}

Week.ISOStringRegExp = /^(\d+)-[wW](\d+)$/;

// See WebCore/platform/DateComponents.h.
Week.Minimum = new Week(1, 1);
Week.Maximum = new Week(275760, 37);

// See WebCore/html/WeekInputType.cpp.
Week.DefaultStep = 604800000;
Week.DefaultStepBase = -259200000;

Week.EpochWeekDay = createUTCDate(1970, 0, 0).getUTCDay();

/**
 * @param {!string} str
 * @return {?Week}
 */
Week.parse = function(str) {
    var match = Week.ISOStringRegExp.exec(str);
    if (!match)
        return null;
    var year = parseInt(match[1], 10);
    var week = parseInt(match[2], 10);
    return new Week(year, week);
};

/**
 * @param {!number} millisecondsSinceEpoch
 * @return {!Week}
 */
Week.createFromValue = function(millisecondsSinceEpoch) {
    return Week.createFromDate(new Date(millisecondsSinceEpoch))
};

/**
 * @param {!Date} date
 * @return {!Week}
 */
Week.createFromDate = function(date) {
    if (isNaN(date.valueOf()))
        throw "Invalid date";
    var year = date.getUTCFullYear();
    if (year <= Week.Maximum.year && Week.weekOneStartDateForYear(year + 1).getTime() <= date.getTime())
        year++;
    else if (year > 1 && Week.weekOneStartDateForYear(year).getTime() > date.getTime())
        year--;
    var week = 1 + Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), date);
    return new Week(year, week);
};

/**
 * @param {!Day} day
 * @return {!Week}
 */
Week.createFromDay = function(day) {
    var year = day.year;
    if (year <= Week.Maximum.year && Week.weekOneStartDayForYear(year + 1) <= day)
        year++;
    else if (year > 1 && Week.weekOneStartDayForYear(year) > day)
        year--;
    var week = Math.floor(1 + (day.valueOf() - Week.weekOneStartDayForYear(year).valueOf()) / MillisecondsPerWeek);
    return new Week(year, week);
};

/**
 * @return {!Week}
 */
Week.createFromToday = function() {
    var now = new Date();
    return Week.createFromDate(createUTCDate(now.getFullYear(), now.getMonth(), now.getDate()));
};

/**
 * @param {!number} year
 * @return {!Date}
 */
Week.weekOneStartDateForYear = function(year) {
    if (year < 1)
        return createUTCDate(1, 0, 1);
    // The week containing January 4th is week one.
    var yearStartDay = createUTCDate(year, 0, 4).getUTCDay();
    return createUTCDate(year, 0, 4 - (yearStartDay + 6) % DaysPerWeek);
};

/**
 * @param {!number} year
 * @return {!Day}
 */
Week.weekOneStartDayForYear = function(year) {
    if (year < 1)
        return Day.Minimum;
    // The week containing January 4th is week one.
    var yearStartDay = createUTCDate(year, 0, 4).getUTCDay();
    return new Day(year, 0, 4 - (yearStartDay + 6) % DaysPerWeek);
};

/**
 * @param {!number} year
 * @return {!number}
 */
Week.numberOfWeeksInYear = function(year) {
    if (year < 1 || year > Week.Maximum.year)
        return 0;
    else if (year === Week.Maximum.year)
        return Week.Maximum.week;
    return Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), Week.weekOneStartDateForYear(year + 1));
};

/**
 * @param {!Date} baseDate
 * @param {!Date} date
 * @return {!number}
 */
Week._numberOfWeeksSinceDate = function(baseDate, date) {
    return Math.floor((date.getTime() - baseDate.getTime()) / MillisecondsPerWeek);
};

/**
 * @param {!DateType} other
 * @return {!boolean}
 */
Week.prototype.equals = function(other) {
    return other instanceof Week && this.year === other.year && this.week === other.week;
};

/**
 * @param {!number=} offset
 * @return {!Week}
 */
Week.prototype.previous = function(offset) {
    if (typeof offset === "undefined")
        offset = 1;
    return new Week(this.year, this.week - offset);
};

/**
 * @param {!number=} offset
 * @return {!Week}
 */
Week.prototype.next = function(offset) {
    if (typeof offset === "undefined")
        offset = 1;
    return new Week(this.year, this.week + offset);
};

/**
 * @return {!Date}
 */
Week.prototype.startDate = function() {
    var weekStartDate = Week.weekOneStartDateForYear(this.year);
    weekStartDate.setUTCDate(weekStartDate.getUTCDate() + (this.week - 1) * 7);
    return weekStartDate;
};

/**
 * @return {!Date}
 */
Week.prototype.endDate = function() {
    if (this.equals(Week.Maximum))
        return Day.Maximum.startDate();
    return this.next().startDate();
};

/**
 * @return {!Day}
 */
Week.prototype.firstDay = function() {
    var weekOneStartDay = Week.weekOneStartDayForYear(this.year);
    return weekOneStartDay.next((this.week - 1) * DaysPerWeek);
};

/**
 * @return {!Day}
 */
Week.prototype.middleDay = function() {
    return this.firstDay().next(3);
};

/**
 * @return {!Day}
 */
Week.prototype.lastDay = function() {
    if (this.equals(Week.Maximum))
        return Day.Maximum;
    return this.next().firstDay().previous();
};

/**
 * @return {!number}
 */
Week.prototype.valueOf = function() {
    return this.firstDay().valueOf() - createUTCDate(1970, 0, 1).getTime();
};

/**
 * @return {!string}
 */
Week.prototype.toString = function() {
    var yearString = String(this.year);
    if (yearString.length < 4)
        yearString = ("000" + yearString).substr(-4, 4);
    return yearString + "-W" + ("0" + this.week).substr(-2, 2);
};

/**
 * @constructor
 * @extends DateType
 * @param {!number} year
 * @param {!number} month
 */
function Month(year, month) { 
    /**
     * @type {number}
     * @const
     */
    this.year = year + Math.floor(month / MonthsPerYear);
    /**
     * @type {number}
     * @const
     */
    this.month = month % MonthsPerYear < 0 ? month % MonthsPerYear + MonthsPerYear : month % MonthsPerYear;
};

Month.ISOStringRegExp = /^(\d+)-(\d+)$/;

// See WebCore/platform/DateComponents.h.
Month.Minimum = new Month(1, 0);
Month.Maximum = new Month(275760, 8);

// See WebCore/html/MonthInputType.cpp.
Month.DefaultStep = 1;
Month.DefaultStepBase = 0;

/**
 * @param {!string} str
 * @return {?Month}
 */
Month.parse = function(str) {
    var match = Month.ISOStringRegExp.exec(str);
    if (!match)
        return null;
    var year = parseInt(match[1], 10);
    var month = parseInt(match[2], 10) - 1;
    return new Month(year, month);
};

/**
 * @param {!number} value
 * @return {!Month}
 */
Month.createFromValue = function(monthsSinceEpoch) {
    return new Month(1970, monthsSinceEpoch)
};

/**
 * @param {!Date} date
 * @return {!Month}
 */
Month.createFromDate = function(date) {
    if (isNaN(date.valueOf()))
        throw "Invalid date";
    return new Month(date.getUTCFullYear(), date.getUTCMonth());
};

/**
 * @param {!Day} day
 * @return {!Month}
 */
Month.createFromDay = function(day) {
    return new Month(day.year, day.month);
};

/**
 * @return {!Month}
 */
Month.createFromToday = function() {
    var now = new Date();
    return new Month(now.getFullYear(), now.getMonth());
};

/**
 * @return {!boolean}
 */
Month.prototype.containsDay = function(day) {
    return this.year === day.year && this.month === day.month;
};

/**
 * @param {!Month} other
 * @return {!boolean}
 */
Month.prototype.equals = function(other) {
    return other instanceof Month && this.year === other.year && this.month === other.month;
};

/**
 * @param {!number=} offset
 * @return {!Month}
 */
Month.prototype.previous = function(offset) {
    if (typeof offset === "undefined")
        offset = 1;
    return new Month(this.year, this.month - offset);
};

/**
 * @param {!number=} offset
 * @return {!Month}
 */
Month.prototype.next = function(offset) {
    if (typeof offset === "undefined")
        offset = 1;
    return new Month(this.year, this.month + offset);
};

/**
 * @return {!Date}
 */
Month.prototype.startDate = function() {
    return createUTCDate(this.year, this.month, 1);
};

/**
 * @return {!Date}
 */
Month.prototype.endDate = function() {
    if (this.equals(Month.Maximum))
        return Day.Maximum.startDate();
    return this.next().startDate();
};

/**
 * @return {!Day}
 */
Month.prototype.firstDay = function() {
    return new Day(this.year, this.month, 1);
};

/**
 * @return {!Day}
 */
Month.prototype.middleDay = function() {
    return new Day(this.year, this.month, this.month === 2 ? 14 : 15);
};

/**
 * @return {!Day}
 */
Month.prototype.lastDay = function() {
    if (this.equals(Month.Maximum))
        return Day.Maximum;
    return this.next().firstDay().previous();
};

/**
 * @return {!number}
 */
Month.prototype.valueOf = function() {
    return (this.year - 1970) * MonthsPerYear + this.month;
};

/**
 * @return {!string}
 */
Month.prototype.toString = function() {
    var yearString = String(this.year);
    if (yearString.length < 4)
        yearString = ("000" + yearString).substr(-4, 4);
    return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2);
};

/**
 * @return {!string}
 */
Month.prototype.toLocaleString = function() {
    if (global.params.locale === "ja")
        return "" + this.year + "年" + formatJapaneseImperialEra(this.year, this.month) + " " + (this.month + 1) + "月";
    return window.pagePopupController.formatMonth(this.year, this.month);
};

/**
 * @return {!string}
 */
Month.prototype.toShortLocaleString = function() {
    return window.pagePopupController.formatShortMonth(this.year, this.month);
};

// ----------------------------------------------------------------
// Initialization

/**
 * @param {Event} event
 */
function handleMessage(event) {
    if (global.argumentsReceived)
        return;
    global.argumentsReceived = true;
    initialize(JSON.parse(event.data));
}

/**
 * @param {!Object} params
 */
function setGlobalParams(params) {
    var name;
    for (name in global.params) {
        if (typeof params[name] === "undefined")
            console.warn("Missing argument: " + name);
    }
    for (name in params) {
        global.params[name] = params[name];
    }
};

/**
 * @param {!Object} args
 */
function initialize(args) { 
    setGlobalParams(args);
    if (global.params.suggestionValues && global.params.suggestionValues.length)
        openSuggestionPicker();
    else
        openCalendarPicker();
}

function closePicker() {
    if (global.picker)
        global.picker.cleanup();
    var main = $("main");
    main.innerHTML = "";
    main.className = "";
};

function openSuggestionPicker() {
    closePicker();
    global.picker = new SuggestionPicker($("main"), global.params);
};

function openCalendarPicker() {
    closePicker();
    global.picker = new CalendarPicker(global.params.mode, global.params);
    global.picker.attachTo($("main"));
};

/**
 * @constructor
 */
function EventEmitter() {
};

/**
 * @param {!string} type
 * @param {!function({...*})} callback
 */
EventEmitter.prototype.on = function(type, callback) {
    console.assert(callback instanceof Function);
    if (!this._callbacks)
        this._callbacks = {};
    if (!this._callbacks[type])
        this._callbacks[type] = [];
    this._callbacks[type].push(callback);
};

EventEmitter.prototype.hasListener = function(type) {
    if (!this._callbacks)
        return false;
    var callbacksForType = this._callbacks[type];
    if (!callbacksForType)
        return false;
    return callbacksForType.length > 0;
};

/**
 * @param {!string} type
 * @param {!function(Object)} callback
 */
EventEmitter.prototype.removeListener = function(type, callback) {
    if (!this._callbacks)
        return;
    var callbacksForType = this._callbacks[type];
    if (!callbacksForType)
        return;
    callbacksForType.splice(callbacksForType.indexOf(callback), 1);
    if (callbacksForType.length === 0)
        delete this._callbacks[type];
};

/**
 * @param {!string} type
 * @param {...*} var_args
 */
EventEmitter.prototype.dispatchEvent = function(type) {
    if (!this._callbacks)
        return;
    var callbacksForType = this._callbacks[type];
    if (!callbacksForType)
        return;
    for (var i = 0; i < callbacksForType.length; ++i) {
        callbacksForType[i].apply(this, Array.prototype.slice.call(arguments, 1));
    }
};

// Parameter t should be a number between 0 and 1.
var AnimationTimingFunction = {
    Linear: function(t){
        return t;
    },
    EaseInOut: function(t){
        t *= 2;
        if (t < 1)
            return Math.pow(t, 3) / 2;
        t -= 2;
        return Math.pow(t, 3) / 2 + 1;
    }
};

/**
 * @constructor
 * @extends EventEmitter
 */
function AnimationManager() {
    EventEmitter.call(this);

    this._isRunning = false;
    this._runningAnimatorCount = 0;
    this._runningAnimators = {};
    this._animationFrameCallbackBound = this._animationFrameCallback.bind(this);
}

AnimationManager.prototype = Object.create(EventEmitter.prototype);

AnimationManager.EventTypeAnimationFrameWillFinish = "animationFrameWillFinish";

AnimationManager.prototype._startAnimation = function() {
    if (this._isRunning)
        return;
    this._isRunning = true;
    window.webkitRequestAnimationFrame(this._animationFrameCallbackBound);
};

AnimationManager.prototype._stopAnimation = function() {
    if (!this._isRunning)
        return;
    this._isRunning = false;
};

/**
 * @param {!Animator} animator
 */
AnimationManager.prototype.add = function(animator) {
    if (this._runningAnimators[animator.id])
        return;
    this._runningAnimators[animator.id] = animator;
    this._runningAnimatorCount++;
    if (this._needsTimer())
        this._startAnimation();
};

/**
 * @param {!Animator} animator
 */
AnimationManager.prototype.remove = function(animator) {
    if (!this._runningAnimators[animator.id])
        return;
    delete this._runningAnimators[animator.id];
    this._runningAnimatorCount--;
    if (!this._needsTimer())
        this._stopAnimation();
};

AnimationManager.prototype._animationFrameCallback = function(now) {
    if (this._runningAnimatorCount > 0) {
        for (var id in this._runningAnimators) {
            this._runningAnimators[id].onAnimationFrame(now);
        }
    }
    this.dispatchEvent(AnimationManager.EventTypeAnimationFrameWillFinish);
    if (this._isRunning)
        window.webkitRequestAnimationFrame(this._animationFrameCallbackBound);
};

/**
 * @return {!boolean}
 */
AnimationManager.prototype._needsTimer = function() {
    return this._runningAnimatorCount > 0 || this.hasListener(AnimationManager.EventTypeAnimationFrameWillFinish);
};

/**
 * @param {!string} type
 * @param {!Function} callback
 * @override
 */
AnimationManager.prototype.on = function(type, callback) {
    EventEmitter.prototype.on.call(this, type, callback);
    if (this._needsTimer())
        this._startAnimation();
};

/**
 * @param {!string} type
 * @param {!Function} callback
 * @override
 */
AnimationManager.prototype.removeListener = function(type, callback) {
    EventEmitter.prototype.removeListener.call(this, type, callback);
    if (!this._needsTimer())
        this._stopAnimation();
};

AnimationManager.shared = new AnimationManager();

/**
 * @constructor
 * @extends EventEmitter
 */
function Animator() {
    EventEmitter.call(this);

    /**
     * @type {!number}
     * @const
     */
    this.id = Animator._lastId++;
    /**
     * @type {!number}
     */
    this.duration = 100;
    /**
     * @type {?function}
     */
    this.step = null;
    /**
     * @type {!boolean}
     * @protected
     */
    this._isRunning = false;
    /**
     * @type {!number}
     */
    this.currentValue = 0;
    /**
     * @type {!number}
     * @protected
     */
    this._lastStepTime = 0;
}

Animator.prototype = Object.create(EventEmitter.prototype);

Animator._lastId = 0;

Animator.EventTypeDidAnimationStop = "didAnimationStop";

/**
 * @return {!boolean}
 */
Animator.prototype.isRunning = function() {
    return this._isRunning;
};

Animator.prototype.start = function() {
    this._lastStepTime = Date.now();
    this._isRunning = true;
    AnimationManager.shared.add(this);
};

Animator.prototype.stop = function() {
    if (!this._isRunning)
        return;
    this._isRunning = false;
    AnimationManager.shared.remove(this);
    this.dispatchEvent(Animator.EventTypeDidAnimationStop, this);
};

/**
 * @param {!number} now
 */
Animator.prototype.onAnimationFrame = function(now) {
    this._lastStepTime = now;
    this.step(this);
};

/**
 * @constructor
 * @extends Animator
 */
function TransitionAnimator() {
    Animator.call(this);
    /**
     * @type {!number}
     * @protected
     */
    this._from = 0;
    /**
     * @type {!number}
     * @protected
     */
    this._to = 0;
    /**
     * @type {!number}
     * @protected
     */
    this._delta = 0;
    /**
     * @type {!number}
     */
    this.progress = 0.0;
    /**
     * @type {!function}
     */
    this.timingFunction = AnimationTimingFunction.Linear;
}

TransitionAnimator.prototype = Object.create(Animator.prototype);

/**
 * @param {!number} value
 */
TransitionAnimator.prototype.setFrom = function(value) {
    this._from = value;
    this._delta = this._to - this._from;
};

TransitionAnimator.prototype.start = function() {
    console.assert(isFinite(this.duration));
    this.progress = 0.0;
    this.currentValue = this._from;
    Animator.prototype.start.call(this);
};

/**
 * @param {!number} value
 */
TransitionAnimator.prototype.setTo = function(value) {
    this._to = value;
    this._delta = this._to - this._from;
};

/**
 * @param {!number} now
 */
TransitionAnimator.prototype.onAnimationFrame = function(now) {
    this.progress += (now - this._lastStepTime) / this.duration;
    this.progress = Math.min(1.0, this.progress);
    this._lastStepTime = now;
    this.currentValue = this.timingFunction(this.progress) * this._delta + this._from;
    this.step(this);
    if (this.progress === 1.0) {
        this.stop();
        return;
    }
};

/**
 * @constructor
 * @extends Animator
 * @param {!number} initialVelocity
 * @param {!number} initialValue
 */
function FlingGestureAnimator(initialVelocity, initialValue) {
    Animator.call(this);
    /**
     * @type {!number}
     */
    this.initialVelocity = initialVelocity;
    /**
     * @type {!number}
     */
    this.initialValue = initialValue;
    /**
     * @type {!number}
     * @protected
     */
    this._elapsedTime = 0;
    var startVelocity = Math.abs(this.initialVelocity);
    if (startVelocity > this._velocityAtTime(0))
        startVelocity = this._velocityAtTime(0);
    if (startVelocity < 0)
        startVelocity = 0;
    /**
     * @type {!number}
     * @protected
     */
    this._timeOffset = this._timeAtVelocity(startVelocity);
    /**
     * @type {!number}
     * @protected
     */
    this._positionOffset = this._valueAtTime(this._timeOffset);
    /**
     * @type {!number}
     */
    this.duration = this._timeAtVelocity(0);
}

FlingGestureAnimator.prototype = Object.create(Animator.prototype);

// Velocity is subject to exponential decay. These parameters are coefficients
// that determine the curve.
FlingGestureAnimator._P0 = -5707.62;
FlingGestureAnimator._P1 = 0.172;
FlingGestureAnimator._P2 = 0.0037;

/**
 * @param {!number} t
 */
FlingGestureAnimator.prototype._valueAtTime = function(t) {
    return FlingGestureAnimator._P0 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1 * t - FlingGestureAnimator._P0;
};

/**
 * @param {!number} t
 */
FlingGestureAnimator.prototype._velocityAtTime = function(t) {
    return -FlingGestureAnimator._P0 * FlingGestureAnimator._P2 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1;
};

/**
 * @param {!number} v
 */
FlingGestureAnimator.prototype._timeAtVelocity = function(v) {
    return -Math.log((v + FlingGestureAnimator._P1) / (-FlingGestureAnimator._P0 * FlingGestureAnimator._P2)) / FlingGestureAnimator._P2;
};

FlingGestureAnimator.prototype.start = function() {
    this._lastStepTime = Date.now();
    Animator.prototype.start.call(this);
};

/**
 * @param {!number} now
 */
FlingGestureAnimator.prototype.onAnimationFrame = function(now) {
    this._elapsedTime += now - this._lastStepTime;
    this._lastStepTime = now;
    if (this._elapsedTime + this._timeOffset >= this.duration) {
        this.stop();
        return;
    }
    var position = this._valueAtTime(this._elapsedTime + this._timeOffset) - this._positionOffset;
    if (this.initialVelocity < 0)
        position = -position;
    this.currentValue = position + this.initialValue;
    this.step(this);
};

/**
 * @constructor
 * @extends EventEmitter
 * @param {?Element} element
 * View adds itself as a property on the element so we can access it from Event.target.
 */
function View(element) {
    EventEmitter.call(this);
    /**
     * @type {Element}
     * @const
     */
    this.element = element || createElement("div");
    this.element.$view = this;
    this.bindCallbackMethods();
}

View.prototype = Object.create(EventEmitter.prototype);

/**
 * @param {!Element} ancestorElement
 * @return {?Object}
 */
View.prototype.offsetRelativeTo = function(ancestorElement) {
    var x = 0;
    var y = 0;
    var element = this.element;
    while (element) {
        x += element.offsetLeft  || 0;
        y += element.offsetTop || 0;
        element = element.offsetParent;
        if (element === ancestorElement)
            return {x: x, y: y};
    }
    return null;
};

/**
 * @param {!View|Node} parent
 * @param {?View|Node=} before
 */
View.prototype.attachTo = function(parent, before) {
    if (parent instanceof View)
        return this.attachTo(parent.element, before);
    if (typeof before === "undefined")
        before = null;
    if (before instanceof View)
        before = before.element;
    parent.insertBefore(this.element, before);
};

View.prototype.bindCallbackMethods = function() {
    for (var methodName in this) {
        if (!/^on[A-Z]/.test(methodName))
            continue;
        if (this.hasOwnProperty(methodName))
            continue;
        var method = this[methodName];
        if (!(method instanceof Function))
            continue;
        this[methodName] = method.bind(this);
    }
};

/**
 * @constructor
 * @extends View
 */
function ScrollView() {
    View.call(this, createElement("div", ScrollView.ClassNameScrollView));
    /**
     * @type {Element}
     * @const
     */
    this.contentElement = createElement("div", ScrollView.ClassNameScrollViewContent);
    this.element.appendChild(this.contentElement);
    /**
     * @type {number}
     */
    this.minimumContentOffset = -Infinity;
    /**
     * @type {number}
     */
    this.maximumContentOffset = Infinity;
    /**
     * @type {number}
     * @protected
     */
    this._contentOffset = 0;
    /**
     * @type {number}
     * @protected
     */
    this._width = 0;
    /**
     * @type {number}
     * @protected
     */
    this._height = 0;
    /**
     * @type {Animator}
     * @protected
     */
    this._scrollAnimator = null;
    /**
     * @type {?Object}
     */
    this.delegate = null;
    /**
     * @type {!number}
     */
    this._lastTouchPosition = 0;
    /**
     * @type {!number}
     */
    this._lastTouchVelocity = 0;
    /**
     * @type {!number}
     */
    this._lastTouchTimeStamp = 0;

    this.element.addEventListener("mousewheel", this.onMouseWheel, false);
    this.element.addEventListener("touchstart", this.onTouchStart, false);

    /**
     * The content offset is partitioned so the it can go beyond the CSS limit
     * of 33554433px.
     * @type {number}
     * @protected
     */
    this._partitionNumber = 0;
}

ScrollView.prototype = Object.create(View.prototype);

ScrollView.PartitionHeight = 100000;
ScrollView.ClassNameScrollView = "scroll-view";
ScrollView.ClassNameScrollViewContent = "scroll-view-content";

/**
 * @param {!Event} event
 */
ScrollView.prototype.onTouchStart = function(event) {
    var touch = event.touches[0];
    this._lastTouchPosition = touch.clientY;
    this._lastTouchVelocity = 0;
    this._lastTouchTimeStamp = event.timeStamp;
    if (this._scrollAnimator)
        this._scrollAnimator.stop();
    window.addEventListener("touchmove", this.onWindowTouchMove, false);
    window.addEventListener("touchend", this.onWindowTouchEnd, false);
};

/**
 * @param {!Event} event
 */
ScrollView.prototype.onWindowTouchMove = function(event) {
    var touch = event.touches[0];
    var deltaTime = event.timeStamp - this._lastTouchTimeStamp;
    var deltaY = this._lastTouchPosition - touch.clientY;
    this.scrollBy(deltaY, false);
    this._lastTouchVelocity = deltaY / deltaTime;
    this._lastTouchPosition = touch.clientY;
    this._lastTouchTimeStamp = event.timeStamp;
    event.stopPropagation();
    event.preventDefault();
};

/**
 * @param {!Event} event
 */
ScrollView.prototype.onWindowTouchEnd = function(event) {
    if (Math.abs(this._lastTouchVelocity) > 0.01) {
        this._scrollAnimator = new FlingGestureAnimator(this._lastTouchVelocity, this._contentOffset);
        this._scrollAnimator.step = this.onFlingGestureAnimatorStep;
        this._scrollAnimator.start();
    }
    window.removeEventListener("touchmove", this.onWindowTouchMove, false);
    window.removeEventListener("touchend", this.onWindowTouchEnd, false);
};

/**
 * @param {!Animator} animator
 */
ScrollView.prototype.onFlingGestureAnimatorStep = function(animator) {
    this.scrollTo(animator.currentValue, false);
};

/**
 * @return {!Animator}
 */
ScrollView.prototype.scrollAnimator = function() {
    return this._scrollAnimator;
};

/**
 * @param {!number} width
 */
ScrollView.prototype.setWidth = function(width) {
    console.assert(isFinite(width));
    if (this._width === width)
        return;
    this._width = width;
    this.element.style.width = this._width + "px";
};

/**
 * @return {!number}
 */
ScrollView.prototype.width = function() {
    return this._width;
};

/**
 * @param {!number} height
 */
ScrollView.prototype.setHeight = function(height) {
    console.assert(isFinite(height));
    if (this._height === height)
        return;
    this._height = height;
    this.element.style.height = height + "px";
    if (this.delegate)
        this.delegate.scrollViewDidChangeHeight(this);
};

/**
 * @return {!number}
 */
ScrollView.prototype.height = function() {
    return this._height;
};

/**
 * @param {!Animator} animator
 */
ScrollView.prototype.onScrollAnimatorStep = function(animator) {
    this.setContentOffset(animator.currentValue);
};

/**
 * @param {!number} offset
 * @param {?boolean} animate
 */
ScrollView.prototype.scrollTo = function(offset, animate) {
    console.assert(isFinite(offset));
    if (!animate) {
        this.setContentOffset(offset);
        return;
    }
    if (this._scrollAnimator)
        this._scrollAnimator.stop();
    this._scrollAnimator = new TransitionAnimator();
    this._scrollAnimator.step = this.onScrollAnimatorStep;
    this._scrollAnimator.setFrom(this._contentOffset);
    this._scrollAnimator.setTo(offset);
    this._scrollAnimator.duration = 300;
    this._scrollAnimator.start();
};

/**
 * @param {!number} offset
 * @param {?boolean} animate
 */
ScrollView.prototype.scrollBy = function(offset, animate) {
    this.scrollTo(this._contentOffset + offset, animate);
};

/**
 * @return {!number}
 */
ScrollView.prototype.contentOffset = function() {
    return this._contentOffset;
};

/**
 * @param {?Event} event
 */
ScrollView.prototype.onMouseWheel = function(event) {
    this.setContentOffset(this._contentOffset - event.wheelDelta / 30);
    event.stopPropagation();
    event.preventDefault();
};


/**
 * @param {!number} value
 */
ScrollView.prototype.setContentOffset = function(value) {
    console.assert(isFinite(value));
    value = Math.min(this.maximumContentOffset - this._height, Math.max(this.minimumContentOffset, Math.floor(value)));
    if (this._contentOffset === value)
        return;
    var newPartitionNumber = Math.floor(value / ScrollView.PartitionHeight);    
    var partitionChanged = this._partitionNumber !== newPartitionNumber;
    this._partitionNumber = newPartitionNumber;
    this._contentOffset = value;
    this.contentElement.style.webkitTransform = "translate(0, " + (-this.contentPositionForContentOffset(this._contentOffset)) + "px)";
    if (this.delegate) {
        this.delegate.scrollViewDidChangeContentOffset(this);
        if (partitionChanged)
            this.delegate.scrollViewDidChangePartition(this);
    }
};

/**
 * @param {!number} offset
 */
ScrollView.prototype.contentPositionForContentOffset = function(offset) {
    return offset - this._partitionNumber * ScrollView.PartitionHeight;
};

/**
 * @constructor
 * @extends View
 */
function ListCell() {
    View.call(this, createElement("div", ListCell.ClassNameListCell));
    
    /**
     * @type {!number}
     */
    this.row = NaN;
    /**
     * @type {!number}
     */
    this._width = 0;
    /**
     * @type {!number}
     */
    this._position = 0;
}

ListCell.prototype = Object.create(View.prototype);

ListCell.DefaultRecycleBinLimit = 64;
ListCell.ClassNameListCell = "list-cell";
ListCell.ClassNameHidden = "hidden";

/**
 * @return {!Array} An array to keep thrown away cells.
 */
ListCell.prototype._recycleBin = function() {
    console.assert(false, "NOT REACHED: ListCell.prototype._recycleBin needs to be overridden.");
    return [];
};

ListCell.prototype.throwAway = function() {
    this.hide();
    var limit = typeof this.constructor.RecycleBinLimit === "undefined" ? ListCell.DefaultRecycleBinLimit : this.constructor.RecycleBinLimit;
    var recycleBin = this._recycleBin();
    if (recycleBin.length < limit)
        recycleBin.push(this);
};

ListCell.prototype.show = function() {
    this.element.classList.remove(ListCell.ClassNameHidden);
};

ListCell.prototype.hide = function() {
    this.element.classList.add(ListCell.ClassNameHidden);
};

/**
 * @return {!number} Width in pixels.
 */
ListCell.prototype.width = function(){
    return this._width;
};

/**
 * @param {!number} width Width in pixels.
 */
ListCell.prototype.setWidth = function(width){
    if (this._width === width)
        return;
    this._width = width;
    this.element.style.width = this._width + "px";
};

/**
 * @return {!number} Position in pixels.
 */
ListCell.prototype.position = function(){
    return this._position;
};

/**
 * @param {!number} y Position in pixels.
 */
ListCell.prototype.setPosition = function(y) {
    if (this._position === y)
        return;
    this._position = y;
    this.element.style.webkitTransform = "translate(0, " + this._position + "px)";
};

/**
 * @param {!boolean} selected
 */
ListCell.prototype.setSelected = function(selected) {
    if (this._selected === selected)
        return;
    this._selected = selected;
    if (this._selected)
        this.element.classList.add("selected");
    else
        this.element.classList.remove("selected");
};

/**
 * @constructor
 * @extends View
 */
function ListView() {
    View.call(this, createElement("div", ListView.ClassNameListView));
    this.element.tabIndex = 0;

    /**
     * @type {!number}
     * @private
     */
    this._width = 0;
    /**
     * @type {!Object}
     * @private
     */
    this._cells = {};

    /**
     * @type {!number}
     */
    this.selectedRow = ListView.NoSelection;

    /**
     * @type {!ScrollView}
     */
    this.scrollView = new ScrollView();
    this.scrollView.delegate = this;
    this.scrollView.minimumContentOffset = 0;
    this.scrollView.setWidth(0);
    this.scrollView.setHeight(0);
    this.scrollView.attachTo(this);

    this.element.addEventListener("click", this.onClick, false);

    /**
     * @type {!boolean}
     * @private
     */
    this._needsUpdateCells = false;
}

ListView.prototype = Object.create(View.prototype);

ListView.NoSelection = -1;
ListView.ClassNameListView = "list-view";

ListView.prototype.onAnimationFrameWillFinish = function() {
    if (this._needsUpdateCells)
        this.updateCells();
};

/**
 * @param {!boolean} needsUpdateCells
 */
ListView.prototype.setNeedsUpdateCells = function(needsUpdateCells) {
    if (this._needsUpdateCells === needsUpdateCells)
        return;
    this._needsUpdateCells = needsUpdateCells;
    if (this._needsUpdateCells)
        AnimationManager.shared.on(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
    else
        AnimationManager.shared.removeListener(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
};

/**
 * @param {!number} row
 * @return {?ListCell}
 */
ListView.prototype.cellAtRow = function(row) {
    return this._cells[row];
};

/**
 * @param {!number} offset Scroll offset in pixels.
 * @return {!number}
 */
ListView.prototype.rowAtScrollOffset = function(offset) {
    console.assert(false, "NOT REACHED: ListView.prototype.rowAtScrollOffset needs to be overridden.");
    return 0;
};

/**
 * @param {!number} row
 * @return {!number} Scroll offset in pixels.
 */
ListView.prototype.scrollOffsetForRow = function(row) {
    console.assert(false, "NOT REACHED: ListView.prototype.scrollOffsetForRow needs to be overridden.");
    return 0;
};

/**
 * @param {!number} row
 * @return {!ListCell}
 */
ListView.prototype.addCellIfNecessary = function(row) {
    var cell = this._cells[row];
    if (cell)
        return cell;
    cell = this.prepareNewCell(row);
    cell.attachTo(this.scrollView.contentElement);
    cell.setWidth(this._width);
    cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(row)));
    this._cells[row] = cell;
    return cell;
};

/**
 * @param {!number} row
 * @return {!ListCell}
 */
ListView.prototype.prepareNewCell = function(row) {
    console.assert(false, "NOT REACHED: ListView.prototype.prepareNewCell should be overridden.");
    return new ListCell();
};

/**
 * @param {!ListCell} cell
 */
ListView.prototype.throwAwayCell = function(cell) {
    delete this._cells[cell.row];
    cell.throwAway();
};

/**
 * @return {!number}
 */
ListView.prototype.firstVisibleRow = function() {
    return this.rowAtScrollOffset(this.scrollView.contentOffset());
};

/**
 * @return {!number}
 */
ListView.prototype.lastVisibleRow = function() {
    return this.rowAtScrollOffset(this.scrollView.contentOffset() + this.scrollView.height() - 1);
};

/**
 * @param {!ScrollView} scrollView
 */
ListView.prototype.scrollViewDidChangeContentOffset = function(scrollView) {
    this.setNeedsUpdateCells(true);
};

/**
 * @param {!ScrollView} scrollView
 */
ListView.prototype.scrollViewDidChangeHeight = function(scrollView) {
    this.setNeedsUpdateCells(true);
};

/**
 * @param {!ScrollView} scrollView
 */
ListView.prototype.scrollViewDidChangePartition = function(scrollView) {
    this.setNeedsUpdateCells(true);
};

ListView.prototype.updateCells = function() {
    var firstVisibleRow = this.firstVisibleRow();
    var lastVisibleRow = this.lastVisibleRow();
    console.assert(firstVisibleRow <= lastVisibleRow);
    for (var c in this._cells) {
        var cell = this._cells[c];
        if (cell.row < firstVisibleRow || cell.row > lastVisibleRow)
            this.throwAwayCell(cell);
    }
    for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
        var cell = this._cells[i];
        if (cell)
            cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
        else
            this.addCellIfNecessary(i);
    }
    this.setNeedsUpdateCells(false);
};

/**
 * @return {!number} Width in pixels.
 */
ListView.prototype.width = function() {
    return this._width;
};

/**
 * @param {!number} width Width in pixels.
 */
ListView.prototype.setWidth = function(width) {
    if (this._width === width)
        return;
    this._width = width;
    this.scrollView.setWidth(this._width);
    for (var c in this._cells) {
        this._cells[c].setWidth(this._width);
    }
    this.element.style.width = this._width + "px";
    this.setNeedsUpdateCells(true);
};

/**
 * @return {!number} Height in pixels.
 */
ListView.prototype.height = function() {
    return this.scrollView.height();
};

/**
 * @param {!number} height Height in pixels.
 */
ListView.prototype.setHeight = function(height) {
    this.scrollView.setHeight(height);
};

/**
 * @param {?Event} event
 */
ListView.prototype.onClick = function(event) {
    var clickedCellElement = enclosingNodeOrSelfWithClass(event.target, ListCell.ClassNameListCell);
    if (!clickedCellElement)
        return;
    var clickedCell = clickedCellElement.$view;
    if (clickedCell.row !== this.selectedRow)
        this.select(clickedCell.row);
};

/**
 * @param {!number} row
 */
ListView.prototype.select = function(row) {
    if (this.selectedRow === row)
        return;
    this.deselect();
    if (row === ListView.NoSelection)
        return;
    this.selectedRow = row;
    var selectedCell = this._cells[this.selectedRow];
    if (selectedCell)
        selectedCell.setSelected(true);
};

ListView.prototype.deselect = function() {
    if (this.selectedRow === ListView.NoSelection)
        return;
    var selectedCell = this._cells[this.selectedRow];
    if (selectedCell)
        selectedCell.setSelected(false);
    this.selectedRow = ListView.NoSelection;
};

/**
 * @param {!number} row
 * @param {!boolean} animate
 */
ListView.prototype.scrollToRow = function(row, animate) {
    this.scrollView.scrollTo(this.scrollOffsetForRow(row), animate);
};

/**
 * @constructor
 * @extends View
 * @param {!ScrollView} scrollView
 */
function ScrubbyScrollBar(scrollView) {
    View.call(this, createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollBar));

    /**
     * @type {!Element}
     * @const
     */
    this.thumb = createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollThumb);
    this.element.appendChild(this.thumb);

    /**
     * @type {!ScrollView}
     * @const
     */
    this.scrollView = scrollView;

    /**
     * @type {!number}
     * @protected
     */
    this._height = 0;
    /**
     * @type {!number}
     * @protected
     */
    this._thumbHeight = 0;
    /**
     * @type {!number}
     * @protected
     */
    this._thumbPosition = 0;

    this.setHeight(0);
    this.setThumbHeight(ScrubbyScrollBar.ThumbHeight);

    /**
     * @type {?Animator}
     * @protected
     */
    this._thumbStyleTopAnimator = null;

    /** 
     * @type {?number}
     * @protected
     */
    this._timer = null;
    
    this.element.addEventListener("mousedown", this.onMouseDown, false);
    this.element.addEventListener("touchstart", this.onTouchStart, false);
}

ScrubbyScrollBar.prototype = Object.create(View.prototype);

ScrubbyScrollBar.ScrollInterval = 16;
ScrubbyScrollBar.ThumbMargin = 2;
ScrubbyScrollBar.ThumbHeight = 30;
ScrubbyScrollBar.ClassNameScrubbyScrollBar = "scrubby-scroll-bar";
ScrubbyScrollBar.ClassNameScrubbyScrollThumb = "scrubby-scroll-thumb";

/**
 * @param {?Event} event
 */
ScrubbyScrollBar.prototype.onTouchStart = function(event) {
    var touch = event.touches[0];
    this._setThumbPositionFromEventPosition(touch.clientY);
    if (this._thumbStyleTopAnimator)
        this._thumbStyleTopAnimator.stop();
    this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval);
    window.addEventListener("touchmove", this.onWindowTouchMove, false);
    window.addEventListener("touchend", this.onWindowTouchEnd, false);
    event.stopPropagation();
    event.preventDefault();
};

/**
 * @param {?Event} event
 */
ScrubbyScrollBar.prototype.onWindowTouchMove = function(event) {
    var touch = event.touches[0];
    this._setThumbPositionFromEventPosition(touch.clientY);
    event.stopPropagation();
    event.preventDefault();
};

/**
 * @param {?Event} event
 */
ScrubbyScrollBar.prototype.onWindowTouchEnd = function(event) {
    this._thumbStyleTopAnimator = new TransitionAnimator();
    this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep;
    this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop);
    this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2);
    this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut;
    this._thumbStyleTopAnimator.duration = 100;
    this._thumbStyleTopAnimator.start();

    window.removeEventListener("touchmove", this.onWindowTouchMove, false);
    window.removeEventListener("touchend", this.onWindowTouchEnd, false);
    clearInterval(this._timer);
};

/**
 * @return {!number} Height of the view in pixels.
 */
ScrubbyScrollBar.prototype.height = function() {
    return this._height;
};

/**
 * @param {!number} height Height of the view in pixels.
 */
ScrubbyScrollBar.prototype.setHeight = function(height) {
    if (this._height === height)
        return;
    this._height = height;
    this.element.style.height = this._height + "px";
    this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px";
    this._thumbPosition = 0;
};

/**
 * @param {!number} height Height of the scroll bar thumb in pixels.
 */
ScrubbyScrollBar.prototype.setThumbHeight = function(height) {
    if (this._thumbHeight === height)
        return;
    this._thumbHeight = height;
    this.thumb.style.height = this._thumbHeight + "px";
    this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px";
    this._thumbPosition = 0;
};

/**
 * @param {number} position
 */
ScrubbyScrollBar.prototype._setThumbPositionFromEventPosition = function(position) {
    var thumbMin = ScrubbyScrollBar.ThumbMargin;
    var thumbMax = this._height - this._thumbHeight - ScrubbyScrollBar.ThumbMargin * 2;
    var y = position - this.element.getBoundingClientRect().top - this.element.clientTop + this.element.scrollTop;
    var thumbPosition = y - this._thumbHeight / 2;
    thumbPosition = Math.max(thumbPosition, thumbMin);
    thumbPosition = Math.min(thumbPosition, thumbMax);
    this.thumb.style.top = thumbPosition + "px";
    this._thumbPosition = 1.0 - (thumbPosition - thumbMin) / (thumbMax - thumbMin) * 2;
};

/**
 * @param {?Event} event
 */
ScrubbyScrollBar.prototype.onMouseDown = function(event) {
    this._setThumbPositionFromEventPosition(event.clientY);

    window.addEventListener("mousemove", this.onWindowMouseMove, false);
    window.addEventListener("mouseup", this.onWindowMouseUp, false);
    if (this._thumbStyleTopAnimator)
        this._thumbStyleTopAnimator.stop();
    this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval);
    event.stopPropagation();
    event.preventDefault();
};

/**
 * @param {?Event} event
 */
ScrubbyScrollBar.prototype.onWindowMouseMove = function(event) {
    this._setThumbPositionFromEventPosition(event.clientY);
};

/**
 * @param {?Event} event
 */
ScrubbyScrollBar.prototype.onWindowMouseUp = function(event) {
    this._thumbStyleTopAnimator = new TransitionAnimator();
    this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep;
    this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop);
    this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2);
    this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut;
    this._thumbStyleTopAnimator.duration = 100;
    this._thumbStyleTopAnimator.start();
    
    window.removeEventListener("mousemove", this.onWindowMouseMove, false);
    window.removeEventListener("mouseup", this.onWindowMouseUp, false);
    clearInterval(this._timer);
};

/**
 * @param {!Animator} animator
 */
ScrubbyScrollBar.prototype.onThumbStyleTopAnimationStep = function(animator) {
    this.thumb.style.top = animator.currentValue + "px";
};

ScrubbyScrollBar.prototype.onScrollTimer = function() {
    var scrollAmount = Math.pow(this._thumbPosition, 2) * 10;
    if (this._thumbPosition > 0)
        scrollAmount = -scrollAmount;
    this.scrollView.scrollBy(scrollAmount, false);
};

/**
 * @constructor
 * @extends ListCell
 * @param {!Array} shortMonthLabels
 */
function YearListCell(shortMonthLabels) {
    ListCell.call(this);
    this.element.classList.add(YearListCell.ClassNameYearListCell);
    this.element.style.height = YearListCell.Height + "px";

    /**
     * @type {!Element}
     * @const
     */
    this.label = createElement("div", YearListCell.ClassNameLabel, "----");
    this.element.appendChild(this.label);
    this.label.style.height = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px";
    this.label.style.lineHeight = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px";

    /**
     * @type {!Array} Array of the 12 month button elements.
     * @const
     */
    this.monthButtons = [];
    var monthChooserElement = createElement("div", YearListCell.ClassNameMonthChooser);
    for (var r = 0; r < YearListCell.ButtonRows; ++r) {
        var buttonsRow = createElement("div", YearListCell.ClassNameMonthButtonsRow);
        for (var c = 0; c < YearListCell.ButtonColumns; ++c) {
            var month = c + r * YearListCell.ButtonColumns;
            var button = createElement("button", YearListCell.ClassNameMonthButton, shortMonthLabels[month]);
            button.dataset.month = month;
            buttonsRow.appendChild(button);
            this.monthButtons.push(button);
        }
        monthChooserElement.appendChild(buttonsRow);
    }
    this.element.appendChild(monthChooserElement);

    /**
     * @type {!boolean}
     * @private
     */
    this._selected = false;
    /**
     * @type {!number}
     * @private
     */
    this._height = 0;
}

YearListCell.prototype = Object.create(ListCell.prototype);

YearListCell.Height = hasInaccuratePointingDevice() ? 31 : 25;
YearListCell.BorderBottomWidth = 1;
YearListCell.ButtonRows = 3;
YearListCell.ButtonColumns = 4;
YearListCell.SelectedHeight = hasInaccuratePointingDevice() ? 127 : 121;
YearListCell.ClassNameYearListCell = "year-list-cell";
YearListCell.ClassNameLabel = "label";
YearListCell.ClassNameMonthChooser = "month-chooser";
YearListCell.ClassNameMonthButtonsRow = "month-buttons-row";
YearListCell.ClassNameMonthButton = "month-button";
YearListCell.ClassNameHighlighted = "highlighted";

YearListCell._recycleBin = [];

/**
 * @return {!Array}
 * @override
 */
YearListCell.prototype._recycleBin = function() {
    return YearListCell._recycleBin;
};

/**
 * @param {!number} row
 */
YearListCell.prototype.reset = function(row) {
    this.row = row;
    this.label.textContent = row + 1;
    for (var i = 0; i < this.monthButtons.length; ++i) {
        this.monthButtons[i].classList.remove(YearListCell.ClassNameHighlighted);
    }
    this.show();
};

/**
 * @return {!number} The height in pixels.
 */
YearListCell.prototype.height = function() {
    return this._height;
};

/**
 * @param {!number} height Height in pixels.
 */
YearListCell.prototype.setHeight = function(height) {
    if (this._height === height)
        return;
    this._height = height;
    this.element.style.height = this._height + "px";
};

/**
 * @constructor
 * @extends ListView
 * @param {!Month} minimumMonth
 * @param {!Month} maximumMonth
 */
function YearListView(minimumMonth, maximumMonth) {
    ListView.call(this);
    this.element.classList.add("year-list-view");

    /**
     * @type {?Month}
     */
    this.highlightedMonth = null;
    /**
     * @type {!Month}
     * @const
     * @protected
     */
    this._minimumMonth = minimumMonth;
    /**
     * @type {!Month}
     * @const
     * @protected
     */
    this._maximumMonth = maximumMonth;

    this.scrollView.minimumContentOffset = (this._minimumMonth.year - 1) * YearListCell.Height;
    this.scrollView.maximumContentOffset = (this._maximumMonth.year - 1) * YearListCell.Height + YearListCell.SelectedHeight;
    
    /**
     * @type {!Object}
     * @const
     * @protected
     */
    this._runningAnimators = {};
    /**
     * @type {!Array}
     * @const
     * @protected
     */
    this._animatingRows = [];
    /**
     * @type {!boolean}
     * @protected
     */
    this._ignoreMouseOutUntillNextMouseOver = false;
    
    /**
     * @type {!ScrubbyScrollBar}
     * @const
     */
    this.scrubbyScrollBar = new ScrubbyScrollBar(this.scrollView);
    this.scrubbyScrollBar.attachTo(this);
    
    this.element.addEventListener("mouseover", this.onMouseOver, false);
    this.element.addEventListener("mouseout", this.onMouseOut, false);
    this.element.addEventListener("keydown", this.onKeyDown, false);
    this.element.addEventListener("touchstart", this.onTouchStart, false);
}

YearListView.prototype = Object.create(ListView.prototype);

YearListView.Height = YearListCell.SelectedHeight - 1;
YearListView.EventTypeYearListViewDidHide = "yearListViewDidHide";
YearListView.EventTypeYearListViewDidSelectMonth = "yearListViewDidSelectMonth";

/**
 * @param {?Event} event
 */
YearListView.prototype.onTouchStart = function(event) {
    var touch = event.touches[0];
    var monthButtonElement = enclosingNodeOrSelfWithClass(touch.target, YearListCell.ClassNameMonthButton);
    if (!monthButtonElement)
        return;
    var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell);
    var cell = cellElement.$view;
    this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10)));
};

/**
 * @param {?Event} event
 */
YearListView.prototype.onMouseOver = function(event) {
    var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
    if (!monthButtonElement)
        return;
    var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell);
    var cell = cellElement.$view;
    this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10)));
    this._ignoreMouseOutUntillNextMouseOver = false;
};

/**
 * @param {?Event} event
 */
YearListView.prototype.onMouseOut = function(event) {
    if (this._ignoreMouseOutUntillNextMouseOver)
        return;
    var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
    if (!monthButtonElement) {
        this.dehighlightMonth();
    }
};

/**
 * @param {!number} width Width in pixels.
 * @override
 */
YearListView.prototype.setWidth = function(width) {
    ListView.prototype.setWidth.call(this, width - this.scrubbyScrollBar.element.offsetWidth);
    this.element.style.width = width + "px";
};

/**
 * @param {!number} height Height in pixels.
 * @override
 */
YearListView.prototype.setHeight = function(height) {
    ListView.prototype.setHeight.call(this, height);
    this.scrubbyScrollBar.setHeight(height);
};

/**
 * @enum {number}
 */
YearListView.RowAnimationDirection = {
    Opening: 0,
    Closing: 1
};

/**
 * @param {!number} row
 * @param {!YearListView.RowAnimationDirection} direction
 */
YearListView.prototype._animateRow = function(row, direction) {
    var fromValue = direction === YearListView.RowAnimationDirection.Closing ? YearListCell.SelectedHeight : YearListCell.Height;
    var oldAnimator = this._runningAnimators[row];
    if (oldAnimator) {
        oldAnimator.stop();
        fromValue = oldAnimator.currentValue;
    }
    var cell = this.cellAtRow(row);
    var animator = new TransitionAnimator();
    animator.step = this.onCellHeightAnimatorStep;
    animator.setFrom(fromValue);
    animator.setTo(direction === YearListView.RowAnimationDirection.Opening ? YearListCell.SelectedHeight : YearListCell.Height);
    animator.timingFunction = AnimationTimingFunction.EaseInOut;
    animator.duration = 300;
    animator.row = row;
    animator.on(Animator.EventTypeDidAnimationStop, this.onCellHeightAnimatorDidStop);
    this._runningAnimators[row] = animator;
    this._animatingRows.push(row);
    this._animatingRows.sort();
    animator.start();
};

/**
 * @param {?Animator} animator
 */
YearListView.prototype.onCellHeightAnimatorDidStop = function(animator) {
    delete this._runningAnimators[animator.row];
    var index = this._animatingRows.indexOf(animator.row);
    this._animatingRows.splice(index, 1);
};

/**
 * @param {!Animator} animator
 */
YearListView.prototype.onCellHeightAnimatorStep = function(animator) {
    var cell = this.cellAtRow(animator.row);
    if (cell)
        cell.setHeight(animator.currentValue);
    this.updateCells();
};

/**
 * @param {?Event} event
 */
YearListView.prototype.onClick = function(event) {
    var oldSelectedRow = this.selectedRow;
    ListView.prototype.onClick.call(this, event);
    var year = this.selectedRow + 1;
    if (this.selectedRow !== oldSelectedRow) {
        var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
        this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
        this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
    } else {
        var monthButton = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
        if (!monthButton)
            return;
        var month = parseInt(monthButton.dataset.month, 10);
        this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
        this.hide();
    }
};

/**
 * @param {!number} scrollOffset
 * @return {!number}
 * @override
 */
YearListView.prototype.rowAtScrollOffset = function(scrollOffset) {
    var remainingOffset = scrollOffset;
    var lastAnimatingRow = 0;
    var rowsWithIrregularHeight = this._animatingRows.slice();
    if (this.selectedRow > -1 && !this._runningAnimators[this.selectedRow]) {
        rowsWithIrregularHeight.push(this.selectedRow);
        rowsWithIrregularHeight.sort();
    }
    for (var i = 0; i < rowsWithIrregularHeight.length; ++i) {
        var row = rowsWithIrregularHeight[i];
        var animator = this._runningAnimators[row];
        var rowHeight = animator ? animator.currentValue : YearListCell.SelectedHeight;
        if (remainingOffset <= (row - lastAnimatingRow) * YearListCell.Height) {
            return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
        }
        remainingOffset -= (row - lastAnimatingRow) * YearListCell.Height;
        if (remainingOffset <= (rowHeight - YearListCell.Height))
            return row;
        remainingOffset -= rowHeight - YearListCell.Height;
        lastAnimatingRow = row;
    }
    return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
};

/**
 * @param {!number} row
 * @return {!number}
 * @override
 */
YearListView.prototype.scrollOffsetForRow = function(row) {
    var scrollOffset = row * YearListCell.Height;
    for (var i = 0; i < this._animatingRows.length; ++i) {
        var animatingRow = this._animatingRows[i];
        if (animatingRow >= row)
            break;
        var animator = this._runningAnimators[animatingRow];
        scrollOffset += animator.currentValue - YearListCell.Height;
    }
    if (this.selectedRow > -1 && this.selectedRow < row && !this._runningAnimators[this.selectedRow]) {
        scrollOffset += YearListCell.SelectedHeight - YearListCell.Height;
    }
    return scrollOffset;
};

/**
 * @param {!number} row
 * @return {!YearListCell}
 * @override
 */
YearListView.prototype.prepareNewCell = function(row) {
    var cell = YearListCell._recycleBin.pop() || new YearListCell(global.params.shortMonthLabels);
    cell.reset(row);
    cell.setSelected(this.selectedRow === row);
    if (this.highlightedMonth && row === this.highlightedMonth.year - 1) {
        cell.monthButtons[this.highlightedMonth.month].classList.add(YearListCell.ClassNameHighlighted);
    }
    for (var i = 0; i < cell.monthButtons.length; ++i) {
        var month = new Month(row + 1, i);
        cell.monthButtons[i].disabled = this._minimumMonth > month || this._maximumMonth < month;
    }
    var animator = this._runningAnimators[row];
    if (animator)
        cell.setHeight(animator.currentValue);
    else if (row === this.selectedRow)
        cell.setHeight(YearListCell.SelectedHeight);
    else
        cell.setHeight(YearListCell.Height);
    return cell;
};

/**
 * @override
 */
YearListView.prototype.updateCells = function() {
    var firstVisibleRow = this.firstVisibleRow();
    var lastVisibleRow = this.lastVisibleRow();
    console.assert(firstVisibleRow <= lastVisibleRow);
    for (var c in this._cells) {
        var cell = this._cells[c];
        if (cell.row < firstVisibleRow || cell.row > lastVisibleRow)
            this.throwAwayCell(cell);
    }
    for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
        var cell = this._cells[i];
        if (cell)
            cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
        else
            this.addCellIfNecessary(i);
    }
    this.setNeedsUpdateCells(false);
};

/**
 * @override
 */
YearListView.prototype.deselect = function() {
    if (this.selectedRow === ListView.NoSelection)
        return;
    var selectedCell = this._cells[this.selectedRow];
    if (selectedCell)
        selectedCell.setSelected(false);
    this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Closing);
    this.selectedRow = ListView.NoSelection;
    this.setNeedsUpdateCells(true);
};

YearListView.prototype.deselectWithoutAnimating = function() {
    if (this.selectedRow === ListView.NoSelection)
        return;
    var selectedCell = this._cells[this.selectedRow];
    if (selectedCell) {
        selectedCell.setSelected(false);
        selectedCell.setHeight(YearListCell.Height);
    }
    this.selectedRow = ListView.NoSelection;
    this.setNeedsUpdateCells(true);
};

/**
 * @param {!number} row
 * @override
 */
YearListView.prototype.select = function(row) {
    if (this.selectedRow === row)
        return;
    this.deselect();
    if (row === ListView.NoSelection)
        return;
    this.selectedRow = row;
    if (this.selectedRow !== ListView.NoSelection) {
        var selectedCell = this._cells[this.selectedRow];
        this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Opening);
        if (selectedCell)
            selectedCell.setSelected(true);
        var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
        this.highlightMonth(new Month(this.selectedRow + 1, month));
    }
    this.setNeedsUpdateCells(true);
};

/**
 * @param {!number} row
 */
YearListView.prototype.selectWithoutAnimating = function(row) {
    if (this.selectedRow === row)
        return;
    this.deselectWithoutAnimating();
    if (row === ListView.NoSelection)
        return;
    this.selectedRow = row;
    if (this.selectedRow !== ListView.NoSelection) {
        var selectedCell = this._cells[this.selectedRow];
        if (selectedCell) {
            selectedCell.setSelected(true);
            selectedCell.setHeight(YearListCell.SelectedHeight);
        }
        var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
        this.highlightMonth(new Month(this.selectedRow + 1, month));
    }
    this.setNeedsUpdateCells(true);
};

/**
 * @param {!Month} month
 * @return {?HTMLButtonElement}
 */
YearListView.prototype.buttonForMonth = function(month) {
    if (!month)
        return null;
    var row = month.year - 1;
    var cell = this.cellAtRow(row);
    if (!cell)
        return null;
    return cell.monthButtons[month.month];
};

YearListView.prototype.dehighlightMonth = function() {
    if (!this.highlightedMonth)
        return;
    var monthButton = this.buttonForMonth(this.highlightedMonth);
    if (monthButton) {
        monthButton.classList.remove(YearListCell.ClassNameHighlighted);
    }
    this.highlightedMonth = null;
};

/**
 * @param {!Month} month
 */
YearListView.prototype.highlightMonth = function(month) {
    if (this.highlightedMonth && this.highlightedMonth.equals(month))
        return;
    this.dehighlightMonth();
    this.highlightedMonth = month;
    if (!this.highlightedMonth)
        return;
    var monthButton = this.buttonForMonth(this.highlightedMonth);
    if (monthButton) {
        monthButton.classList.add(YearListCell.ClassNameHighlighted);
    }
};

/**
 * @param {!Month} month
 */
YearListView.prototype.show = function(month) {
    this._ignoreMouseOutUntillNextMouseOver = true;
    
    this.scrollToRow(month.year - 1, false);
    this.selectWithoutAnimating(month.year - 1);
    this.highlightMonth(month);
};

YearListView.prototype.hide = function() {
    this.dispatchEvent(YearListView.EventTypeYearListViewDidHide, this);
};

/**
 * @param {!Month} month
 */
YearListView.prototype._moveHighlightTo = function(month) {
    this.highlightMonth(month);
    this.select(this.highlightedMonth.year - 1);

    this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, month);
    this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
    return true;
};

/**
 * @param {?Event} event
 */
YearListView.prototype.onKeyDown = function(event) {
    var key = event.keyIdentifier;
    var eventHandled = false;
    if (key == "U+0054") // 't' key.
        eventHandled = this._moveHighlightTo(Month.createFromToday());
    else if (this.highlightedMonth) {
        if (global.params.isLocaleRTL ? key == "Right" : key == "Left")
            eventHandled = this._moveHighlightTo(this.highlightedMonth.previous());
        else if (key == "Up")
            eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(YearListCell.ButtonColumns));
        else if (global.params.isLocaleRTL ? key == "Left" : key == "Right")
            eventHandled = this._moveHighlightTo(this.highlightedMonth.next());
        else if (key == "Down")
            eventHandled = this._moveHighlightTo(this.highlightedMonth.next(YearListCell.ButtonColumns));
        else if (key == "PageUp")
            eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(MonthsPerYear));
        else if (key == "PageDown")
            eventHandled = this._moveHighlightTo(this.highlightedMonth.next(MonthsPerYear));
        else if (key == "Enter") {
            this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, this.highlightedMonth);
            this.hide();
            eventHandled = true;
        }
    } else if (key == "Up") {
        this.scrollView.scrollBy(-YearListCell.Height, true);
        eventHandled = true;
    } else if (key == "Down") {
        this.scrollView.scrollBy(YearListCell.Height, true);
        eventHandled = true;
    } else if (key == "PageUp") {
        this.scrollView.scrollBy(-this.scrollView.height(), true);
        eventHandled = true;
    } else if (key == "PageDown") {
        this.scrollView.scrollBy(this.scrollView.height(), true);
        eventHandled = true;
    }

    if (eventHandled) {
        event.stopPropagation();
        event.preventDefault();
    }
};

/**
 * @constructor
 * @extends View
 * @param {!Month} minimumMonth
 * @param {!Month} maximumMonth
 */
function MonthPopupView(minimumMonth, maximumMonth) {
    View.call(this, createElement("div", MonthPopupView.ClassNameMonthPopupView));

    /**
     * @type {!YearListView}
     * @const
     */
    this.yearListView = new YearListView(minimumMonth, maximumMonth);
    this.yearListView.attachTo(this);

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

    this.element.addEventListener("click", this.onClick, false);
}

MonthPopupView.prototype = Object.create(View.prototype);

MonthPopupView.ClassNameMonthPopupView = "month-popup-view";

MonthPopupView.prototype.show = function(initialMonth, calendarTableRect) {
    this.isVisible = true;
    document.body.appendChild(this.element);
    this.yearListView.setWidth(calendarTableRect.width - 2);
    this.yearListView.setHeight(YearListView.Height);
    if (global.params.isLocaleRTL)
        this.yearListView.element.style.right = calendarTableRect.x + "px";
    else
        this.yearListView.element.style.left = calendarTableRect.x + "px";
    this.yearListView.element.style.top = calendarTableRect.y + "px";
    this.yearListView.show(initialMonth);
    this.yearListView.element.focus();
};

MonthPopupView.prototype.hide = function() {
    if (!this.isVisible)
        return;
    this.isVisible = false;
    this.element.parentNode.removeChild(this.element);
    this.yearListView.hide();
};

/**
 * @param {?Event} event
 */
MonthPopupView.prototype.onClick = function(event) {
    if (event.target !== this.element)
        return;
    this.hide();
};

/**
 * @constructor
 * @extends View
 * @param {!number} maxWidth Maximum width in pixels.
 */
function MonthPopupButton(maxWidth) {
    View.call(this, createElement("button", MonthPopupButton.ClassNameMonthPopupButton));

    /**
     * @type {!Element}
     * @const
     */
    this.labelElement = createElement("span", MonthPopupButton.ClassNameMonthPopupButtonLabel, "-----");
    this.element.appendChild(this.labelElement);

    /**
     * @type {!Element}
     * @const
     */
    this.disclosureTriangleIcon = createElement("span", MonthPopupButton.ClassNameDisclosureTriangle);
    this.disclosureTriangleIcon.innerHTML = "<svg width='7' height='5'><polygon points='0,1 7,1 3.5,5' style='fill:#000000;' /></svg>";
    this.element.appendChild(this.disclosureTriangleIcon);

    /**
     * @type {!boolean}
     * @protected
     */
    this._useShortMonth = this._shouldUseShortMonth(maxWidth);
    this.element.style.maxWidth = maxWidth + "px";

    this.element.addEventListener("click", this.onClick, false);
}

MonthPopupButton.prototype = Object.create(View.prototype);

MonthPopupButton.ClassNameMonthPopupButton = "month-popup-button";
MonthPopupButton.ClassNameMonthPopupButtonLabel = "month-popup-button-label";
MonthPopupButton.ClassNameDisclosureTriangle = "disclosure-triangle";
MonthPopupButton.EventTypeButtonClick = "buttonClick";

/**
 * @param {!number} maxWidth Maximum available width in pixels.
 * @return {!boolean}
 */
MonthPopupButton.prototype._shouldUseShortMonth = function(maxWidth) {
    document.body.appendChild(this.element);
    var month = Month.Maximum;
    for (var i = 0; i < MonthsPerYear; ++i) {
        this.labelElement.textContent = month.toLocaleString();
        if (this.element.offsetWidth > maxWidth)
            return true;
        month = month.previous();
    }
    document.body.removeChild(this.element);
    return false;
};

/**
 * @param {!Month} month
 */
MonthPopupButton.prototype.setCurrentMonth = function(month) {
    this.labelElement.textContent = this._useShortMonth ? month.toShortLocaleString() : month.toLocaleString();
};

/**
 * @param {?Event} event
 */
MonthPopupButton.prototype.onClick = function(event) {
    this.dispatchEvent(MonthPopupButton.EventTypeButtonClick, this);
};

/**
 * @constructor
 * @extends View
 */
function CalendarNavigationButton() {
    View.call(this, createElement("button", CalendarNavigationButton.ClassNameCalendarNavigationButton));
    /**
     * @type {number} Threshold for starting repeating clicks in milliseconds.
     */
    this.repeatingClicksStartingThreshold = CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold;
    /**
     * @type {number} Interval between reapeating clicks in milliseconds.
     */
    this.reapeatingClicksInterval = CalendarNavigationButton.DefaultRepeatingClicksInterval;
    /**
     * @type {?number} The ID for the timeout that triggers the repeating clicks.
     */
    this._timer = null;
    this.element.addEventListener("click", this.onClick, false);
    this.element.addEventListener("mousedown", this.onMouseDown, false);
    this.element.addEventListener("touchstart", this.onTouchStart, false);
};

CalendarNavigationButton.prototype = Object.create(View.prototype);

CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold = 600;
CalendarNavigationButton.DefaultRepeatingClicksInterval = 300;
CalendarNavigationButton.LeftMargin = 4;
CalendarNavigationButton.Width = 24;
CalendarNavigationButton.ClassNameCalendarNavigationButton = "calendar-navigation-button";
CalendarNavigationButton.EventTypeButtonClick = "buttonClick";
CalendarNavigationButton.EventTypeRepeatingButtonClick = "repeatingButtonClick";

/**
 * @param {!boolean} disabled
 */
CalendarNavigationButton.prototype.setDisabled = function(disabled) {
    this.element.disabled = disabled;
};

/**
 * @param {?Event} event
 */
CalendarNavigationButton.prototype.onClick = function(event) {
    this.dispatchEvent(CalendarNavigationButton.EventTypeButtonClick, this);
};

/**
 * @param {?Event} event
 */
CalendarNavigationButton.prototype.onTouchStart = function(event) {
    if (this._timer !== null)
        return;
    this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
    window.addEventListener("touchend", this.onWindowTouchEnd, false);
};

/**
 * @param {?Event} event
 */
CalendarNavigationButton.prototype.onWindowTouchEnd = function(event) {
    if (this._timer === null)
        return;
    clearTimeout(this._timer);
    this._timer = null;
    window.removeEventListener("touchend", this.onWindowMouseUp, false);
};

/**
 * @param {?Event} event
 */
CalendarNavigationButton.prototype.onMouseDown = function(event) {
    if (this._timer !== null)
        return;
    this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
    window.addEventListener("mouseup", this.onWindowMouseUp, false);
};

/**
 * @param {?Event} event
 */
CalendarNavigationButton.prototype.onWindowMouseUp = function(event) {
    if (this._timer === null)
        return;
    clearTimeout(this._timer);
    this._timer = null;
    window.removeEventListener("mouseup", this.onWindowMouseUp, false);
};

/**
 * @param {?Event} event
 */
CalendarNavigationButton.prototype.onRepeatingClick = function(event) {
    this.dispatchEvent(CalendarNavigationButton.EventTypeRepeatingButtonClick, this);
    this._timer = setTimeout(this.onRepeatingClick, this.reapeatingClicksInterval);
};

/**
 * @constructor
 * @extends View
 * @param {!CalendarPicker} calendarPicker
 */
function CalendarHeaderView(calendarPicker) {
    View.call(this, createElement("div", CalendarHeaderView.ClassNameCalendarHeaderView));
    this.calendarPicker = calendarPicker;
    this.calendarPicker.on(CalendarPicker.EventTypeCurrentMonthChanged, this.onCurrentMonthChanged);
    
    var titleElement = createElement("div", CalendarHeaderView.ClassNameCalendarTitle);
    this.element.appendChild(titleElement);

    /**
     * @type {!MonthPopupButton}
     */
    this.monthPopupButton = new MonthPopupButton(this.calendarPicker.calendarTableView.width() - CalendarTableView.BorderWidth * 2 - CalendarNavigationButton.Width * 3 - CalendarNavigationButton.LeftMargin * 2);
    this.monthPopupButton.attachTo(titleElement);

    /**
     * @type {!CalendarNavigationButton}
     * @const
     */
    this._previousMonthButton = new CalendarNavigationButton();
    this._previousMonthButton.attachTo(this);
    this._previousMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
    this._previousMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick);

    /**
     * @type {!CalendarNavigationButton}
     * @const
     */
    this._todayButton = new CalendarNavigationButton();
    this._todayButton.attachTo(this);
    this._todayButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
    this._todayButton.element.classList.add(CalendarHeaderView.ClassNameTodayButton);
    var monthContainingToday = Month.createFromToday();
    this._todayButton.setDisabled(monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth);

    /**
     * @type {!CalendarNavigationButton}
     * @const
     */
    this._nextMonthButton = new CalendarNavigationButton();
    this._nextMonthButton.attachTo(this);
    this._nextMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
    this._nextMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick);

    if (global.params.isLocaleRTL) {
        this._nextMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
        this._previousMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
    } else {
        this._nextMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
        this._previousMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
    }
}

CalendarHeaderView.prototype = Object.create(View.prototype);

CalendarHeaderView.Height = 24;
CalendarHeaderView.BottomMargin = 10;
CalendarHeaderView._ForwardTriangle = "<svg width='4' height='7'><polygon points='0,7 0,0, 4,3.5' style='fill:#6e6e6e;' /></svg>";
CalendarHeaderView._BackwardTriangle = "<svg width='4' height='7'><polygon points='0,3.5 4,7 4,0' style='fill:#6e6e6e;' /></svg>";
CalendarHeaderView.ClassNameCalendarHeaderView = "calendar-header-view";
CalendarHeaderView.ClassNameCalendarTitle = "calendar-title";
CalendarHeaderView.ClassNameTodayButton = "today-button";

CalendarHeaderView.prototype.onCurrentMonthChanged = function() {
    this.monthPopupButton.setCurrentMonth(this.calendarPicker.currentMonth());
    this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth);
    this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth);
};

CalendarHeaderView.prototype.onNavigationButtonClick = function(sender) {
    if (sender === this._previousMonthButton)
        this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().previous(), CalendarPicker.NavigationBehavior.WithAnimation);
    else if (sender === this._nextMonthButton)
        this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().next(), CalendarPicker.NavigationBehavior.WithAnimation);
    else
        this.calendarPicker.selectRangeContainingDay(Day.createFromToday());
};

/**
 * @param {!boolean} disabled
 */
CalendarHeaderView.prototype.setDisabled = function(disabled) {
    this.disabled = disabled;
    this.monthPopupButton.element.disabled = this.disabled;
    this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth);
    this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth);
    var monthContainingToday = Month.createFromToday();
    this._todayButton.setDisabled(this.disabled || monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth);
};

/**
 * @constructor
 * @extends ListCell
 */
function DayCell() {
    ListCell.call(this);
    this.element.classList.add(DayCell.ClassNameDayCell);
    this.element.style.width = DayCell.Width + "px";
    this.element.style.height = DayCell.Height + "px";
    this.element.style.lineHeight = (DayCell.Height - DayCell.PaddingSize * 2) + "px";
    /**
     * @type {?Day}
     */
    this.day = null;
};

DayCell.prototype = Object.create(ListCell.prototype);

DayCell.Width = 34;
DayCell.Height = hasInaccuratePointingDevice() ? 34 : 20;
DayCell.PaddingSize = 1;
DayCell.ClassNameDayCell = "day-cell";
DayCell.ClassNameHighlighted = "highlighted";
DayCell.ClassNameDisabled = "disabled";
DayCell.ClassNameCurrentMonth = "current-month";
DayCell.ClassNameToday = "today";

DayCell._recycleBin = [];

DayCell.recycleOrCreate = function() {
    return DayCell._recycleBin.pop() || new DayCell();
};

/**
 * @return {!Array}
 * @override
 */
DayCell.prototype._recycleBin = function() {
    return DayCell._recycleBin;
};

/**
 * @override
 */
DayCell.prototype.throwAway = function() {
    ListCell.prototype.throwAway.call(this);
    this.day = null;
};

/**
 * @param {!boolean} highlighted
 */
DayCell.prototype.setHighlighted = function(highlighted) {
    if (highlighted)
        this.element.classList.add(DayCell.ClassNameHighlighted);
    else
        this.element.classList.remove(DayCell.ClassNameHighlighted);
};

/**
 * @param {!boolean} disabled
 */
DayCell.prototype.setDisabled = function(disabled) {
    if (disabled)
        this.element.classList.add(DayCell.ClassNameDisabled);
    else
        this.element.classList.remove(DayCell.ClassNameDisabled);
};

/**
 * @param {!boolean} selected
 */
DayCell.prototype.setIsInCurrentMonth = function(selected) {
    if (selected)
        this.element.classList.add(DayCell.ClassNameCurrentMonth);
    else
        this.element.classList.remove(DayCell.ClassNameCurrentMonth);
};

/**
 * @param {!boolean} selected
 */
DayCell.prototype.setIsToday = function(selected) {
    if (selected)
        this.element.classList.add(DayCell.ClassNameToday);
    else
        this.element.classList.remove(DayCell.ClassNameToday);
};

/**
 * @param {!Day} day
 */
DayCell.prototype.reset = function(day) {
    this.day = day;
    this.element.textContent = localizeNumber(this.day.date.toString());
    this.show();
};

/**
 * @constructor
 * @extends ListCell
 */
function WeekNumberCell() {
    ListCell.call(this);
    this.element.classList.add(WeekNumberCell.ClassNameWeekNumberCell);
    this.element.style.width = (WeekNumberCell.Width - WeekNumberCell.SeparatorWidth) + "px";
    this.element.style.height = WeekNumberCell.Height + "px";
    this.element.style.lineHeight = (WeekNumberCell.Height - WeekNumberCell.PaddingSize * 2) + "px";
    /**
     * @type {?Week}
     */
    this.week = null;
};

WeekNumberCell.prototype = Object.create(ListCell.prototype);

WeekNumberCell.Width = 48;
WeekNumberCell.Height = DayCell.Height;
WeekNumberCell.SeparatorWidth = 1;
WeekNumberCell.PaddingSize = 1;
WeekNumberCell.ClassNameWeekNumberCell = "week-number-cell";
WeekNumberCell.ClassNameHighlighted = "highlighted";
WeekNumberCell.ClassNameDisabled = "disabled";

WeekNumberCell._recycleBin = [];

/**
 * @return {!Array}
 * @override
 */
WeekNumberCell.prototype._recycleBin = function() {
    return WeekNumberCell._recycleBin;
};

/**
 * @return {!WeekNumberCell}
 */
WeekNumberCell.recycleOrCreate = function() {
    return WeekNumberCell._recycleBin.pop() || new WeekNumberCell();
};

/**
 * @param {!Week} week
 */
WeekNumberCell.prototype.reset = function(week) {
    this.week = week;
    this.element.textContent = localizeNumber(this.week.week.toString());
    this.show();
};

/**
 * @override
 */
WeekNumberCell.prototype.throwAway = function() {
    ListCell.prototype.throwAway.call(this);
    this.week = null;
};

WeekNumberCell.prototype.setHighlighted = function(highlighted) {
    if (highlighted)
        this.element.classList.add(WeekNumberCell.ClassNameHighlighted);
    else
        this.element.classList.remove(WeekNumberCell.ClassNameHighlighted);
};

WeekNumberCell.prototype.setDisabled = function(disabled) {
    if (disabled)
        this.element.classList.add(WeekNumberCell.ClassNameDisabled);
    else
        this.element.classList.remove(WeekNumberCell.ClassNameDisabled);
};

/**
 * @constructor
 * @extends View
 * @param {!boolean} hasWeekNumberColumn
 */
function CalendarTableHeaderView(hasWeekNumberColumn) {
    View.call(this, createElement("div", "calendar-table-header-view"));
    if (hasWeekNumberColumn) {
        var weekNumberLabelElement = createElement("div", "week-number-label", global.params.weekLabel);
        weekNumberLabelElement.style.width = WeekNumberCell.Width + "px";
        this.element.appendChild(weekNumberLabelElement);
    }
    for (var i = 0; i < DaysPerWeek; ++i) {
        var weekDayNumber = (global.params.weekStartDay + i) % DaysPerWeek;
        var labelElement = createElement("div", "week-day-label", global.params.dayLabels[weekDayNumber]);
        labelElement.style.width = DayCell.Width + "px";
        this.element.appendChild(labelElement);
        if (getLanguage() === "ja") {
            if (weekDayNumber === 0)
                labelElement.style.color = "red";
            else if (weekDayNumber === 6)
                labelElement.style.color = "blue";
        }
    }
}

CalendarTableHeaderView.prototype = Object.create(View.prototype);

CalendarTableHeaderView.Height = 25;

/**
 * @constructor
 * @extends ListCell
 */
function CalendarRowCell() {
    ListCell.call(this);
    this.element.classList.add(CalendarRowCell.ClassNameCalendarRowCell);
    this.element.style.height = CalendarRowCell.Height + "px";

    /**
     * @type {!Array}
     * @protected
     */
    this._dayCells = [];
    /**
     * @type {!number}
     */
    this.row = 0;
    /**
     * @type {?CalendarTableView}
     */
    this.calendarTableView = null;
}

CalendarRowCell.prototype = Object.create(ListCell.prototype);

CalendarRowCell.Height = DayCell.Height;
CalendarRowCell.ClassNameCalendarRowCell = "calendar-row-cell";

CalendarRowCell._recycleBin = [];

/**
 * @return {!Array}
 * @override
 */
CalendarRowCell.prototype._recycleBin = function() {
    return CalendarRowCell._recycleBin;
};

/**
 * @param {!number} row
 * @param {!CalendarTableView} calendarTableView
 */
CalendarRowCell.prototype.reset = function(row, calendarTableView) {
    this.row = row;
    this.calendarTableView = calendarTableView;
    if (this.calendarTableView.hasWeekNumberColumn) {
        var middleDay = this.calendarTableView.dayAtColumnAndRow(3, row);
        var week = Week.createFromDay(middleDay);
        this.weekNumberCell = this.calendarTableView.prepareNewWeekNumberCell(week);
        this.weekNumberCell.attachTo(this);
    }
    var day = calendarTableView.dayAtColumnAndRow(0, row);
    for (var i = 0; i < DaysPerWeek; ++i) {
        var dayCell = this.calendarTableView.prepareNewDayCell(day);
        dayCell.attachTo(this);
        this._dayCells.push(dayCell);
        day = day.next();
    }
    this.show();
};

/**
 * @override
 */
CalendarRowCell.prototype.throwAway = function() {
    ListCell.prototype.throwAway.call(this);
    if (this.weekNumberCell)
        this.calendarTableView.throwAwayWeekNumberCell(this.weekNumberCell);
    this._dayCells.forEach(this.calendarTableView.throwAwayDayCell, this.calendarTableView);
    this._dayCells.length = 0;
};

/**
 * @constructor
 * @extends ListView
 * @param {!CalendarPicker} calendarPicker
 */
function CalendarTableView(calendarPicker) {
    ListView.call(this);
    this.element.classList.add(CalendarTableView.ClassNameCalendarTableView);
    this.element.tabIndex = 0;

    /**
     * @type {!boolean}
     * @const
     */
    this.hasWeekNumberColumn = calendarPicker.type === "week";
    /**
     * @type {!CalendarPicker}
     * @const
     */
    this.calendarPicker = calendarPicker;
    /**
     * @type {!Object}
     * @const
     */
    this._dayCells = {};
    var headerView = new CalendarTableHeaderView(this.hasWeekNumberColumn);
    headerView.attachTo(this, this.scrollView);

    if (this.hasWeekNumberColumn) {
        this.setWidth(DayCell.Width * DaysPerWeek + WeekNumberCell.Width);
        /**
         * @type {?Array}
         * @const
         */
        this._weekNumberCells = [];
    } else {
        this.setWidth(DayCell.Width * DaysPerWeek);
    }
    
    /**
     * @type {!boolean}
     * @protected
     */
    this._ignoreMouseOutUntillNextMouseOver = false;

    this.element.addEventListener("click", this.onClick, false);
    this.element.addEventListener("mouseover", this.onMouseOver, false);
    this.element.addEventListener("mouseout", this.onMouseOut, false);

    // You shouldn't be able to use the mouse wheel to scroll.
    this.scrollView.element.removeEventListener("mousewheel", this.scrollView.onMouseWheel, false);
    // You shouldn't be able to do gesture scroll.
    this.scrollView.element.removeEventListener("touchstart", this.scrollView.onTouchStart, false);
}

CalendarTableView.prototype = Object.create(ListView.prototype);

CalendarTableView.BorderWidth = 1;
CalendarTableView.ClassNameCalendarTableView = "calendar-table-view";

/**
 * @param {!number} scrollOffset
 * @return {!number}
 */
CalendarTableView.prototype.rowAtScrollOffset = function(scrollOffset) {
    return Math.floor(scrollOffset / CalendarRowCell.Height);
};

/**
 * @param {!number} row
 * @return {!number}
 */
CalendarTableView.prototype.scrollOffsetForRow = function(row) {
    return row * CalendarRowCell.Height;
};

/**
 * @param {?Event} event
 */
CalendarTableView.prototype.onClick = function(event) {
    if (this.hasWeekNumberColumn) {
        var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell);
        if (weekNumberCellElement) {
            var weekNumberCell = weekNumberCellElement.$view;
            this.calendarPicker.selectRangeContainingDay(weekNumberCell.week.firstDay());
            return;
        }
    }
    var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
    if (!dayCellElement)
        return;
    var dayCell = dayCellElement.$view;
    this.calendarPicker.selectRangeContainingDay(dayCell.day);
};

/**
 * @param {?Event} event
 */
CalendarTableView.prototype.onMouseOver = function(event) {
    if (this.hasWeekNumberColumn) {
        var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell);
        if (weekNumberCellElement) {
            var weekNumberCell = weekNumberCellElement.$view;
            this.calendarPicker.highlightRangeContainingDay(weekNumberCell.week.firstDay());
            this._ignoreMouseOutUntillNextMouseOver = false;
            return;
        }
    }
    var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
    if (!dayCellElement)
        return;
    var dayCell = dayCellElement.$view;
    this.calendarPicker.highlightRangeContainingDay(dayCell.day);
    this._ignoreMouseOutUntillNextMouseOver = false;
};

/**
 * @param {?Event} event
 */
CalendarTableView.prototype.onMouseOut = function(event) {
    if (this._ignoreMouseOutUntillNextMouseOver)
        return;
    var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
    if (!dayCellElement) {
        this.calendarPicker.highlightRangeContainingDay(null);
    }
};

/**
 * @param {!number} row
 * @return {!CalendarRowCell}
 */
CalendarTableView.prototype.prepareNewCell = function(row) {
    var cell = CalendarRowCell._recycleBin.pop() || new CalendarRowCell();
    cell.reset(row, this);
    return cell;
};

/**
 * @return {!number} Height in pixels.
 */
CalendarTableView.prototype.height = function() {
    return this.scrollView.height() + CalendarTableHeaderView.Height + CalendarTableView.BorderWidth * 2;
};

/**
 * @param {!number} height Height in pixels.
 */
CalendarTableView.prototype.setHeight = function(height) {
    this.scrollView.setHeight(height - CalendarTableHeaderView.Height - CalendarTableView.BorderWidth * 2);
};

/**
 * @param {!Month} month
 * @param {!boolean} animate
 */
CalendarTableView.prototype.scrollToMonth = function(month, animate) {
    var rowForFirstDayInMonth = this.columnAndRowForDay(month.firstDay()).row;
    this.scrollView.scrollTo(this.scrollOffsetForRow(rowForFirstDayInMonth), animate);
};

/**
 * @param {!number} column
 * @param {!number} row
 * @return {!Day}
 */
CalendarTableView.prototype.dayAtColumnAndRow = function(column, row) {
    var daysSinceMinimum = row * DaysPerWeek + column + global.params.weekStartDay - CalendarTableView._MinimumDayWeekDay;
    return Day.createFromValue(daysSinceMinimum * MillisecondsPerDay + CalendarTableView._MinimumDayValue);
};

CalendarTableView._MinimumDayValue = Day.Minimum.valueOf();
CalendarTableView._MinimumDayWeekDay = Day.Minimum.weekDay();

/**
 * @param {!Day} day
 * @return {!Object} Object with properties column and row.
 */
CalendarTableView.prototype.columnAndRowForDay = function(day) {
    var daysSinceMinimum = (day.valueOf() - CalendarTableView._MinimumDayValue) / MillisecondsPerDay;
    var offset = daysSinceMinimum + CalendarTableView._MinimumDayWeekDay - global.params.weekStartDay;
    var row = Math.floor(offset / DaysPerWeek);
    var column = offset - row * DaysPerWeek;
    return {
        column: column,
        row: row
    };
};

CalendarTableView.prototype.updateCells = function() {
    ListView.prototype.updateCells.call(this);

    var selection = this.calendarPicker.selection();
    var firstDayInSelection;
    var lastDayInSelection;
    if (selection) {
        firstDayInSelection = selection.firstDay().valueOf();
        lastDayInSelection = selection.lastDay().valueOf();
    } else {
        firstDayInSelection = Infinity;
        lastDayInSelection = Infinity;
    }
    var highlight = this.calendarPicker.highlight();
    var firstDayInHighlight;
    var lastDayInHighlight;
    if (highlight) {
        firstDayInHighlight = highlight.firstDay().valueOf();
        lastDayInHighlight = highlight.lastDay().valueOf();
    } else {
        firstDayInHighlight = Infinity;
        lastDayInHighlight = Infinity;
    }
    var currentMonth = this.calendarPicker.currentMonth();
    var firstDayInCurrentMonth = currentMonth.firstDay().valueOf();
    var lastDayInCurrentMonth = currentMonth.lastDay().valueOf();
    for (var dayString in this._dayCells) {
        var dayCell = this._dayCells[dayString];
        var day = dayCell.day;
        dayCell.setIsToday(Day.createFromToday().equals(day));
        dayCell.setSelected(day >= firstDayInSelection && day <= lastDayInSelection);
        dayCell.setHighlighted(day >= firstDayInHighlight && day <= lastDayInHighlight);
        dayCell.setIsInCurrentMonth(day >= firstDayInCurrentMonth && day <= lastDayInCurrentMonth);
        dayCell.setDisabled(!this.calendarPicker.isValidDay(day));
    }
    if (this.hasWeekNumberColumn) {
        for (var weekString in this._weekNumberCells) {
            var weekNumberCell = this._weekNumberCells[weekString];
            var week = weekNumberCell.week;
            weekNumberCell.setSelected(selection && selection.equals(week));
            weekNumberCell.setHighlighted(highlight && highlight.equals(week));
            weekNumberCell.setDisabled(!this.calendarPicker.isValid(week));
        }
    }
};

/**
 * @param {!Day} day
 * @return {!DayCell}
 */
CalendarTableView.prototype.prepareNewDayCell = function(day) {
    var dayCell = DayCell.recycleOrCreate();
    dayCell.reset(day);
    this._dayCells[dayCell.day.toString()] = dayCell;
    return dayCell;
};

/**
 * @param {!Week} week
 * @return {!WeekNumberCell}
 */
CalendarTableView.prototype.prepareNewWeekNumberCell = function(week) {
    var weekNumberCell = WeekNumberCell.recycleOrCreate();
    weekNumberCell.reset(week);
    this._weekNumberCells[weekNumberCell.week.toString()] = weekNumberCell;
    return weekNumberCell;
};

/**
 * @param {!DayCell} dayCell
 */
CalendarTableView.prototype.throwAwayDayCell = function(dayCell) {
    delete this._dayCells[dayCell.day.toString()];
    dayCell.throwAway();
};

/**
 * @param {!WeekNumberCell} weekNumberCell
 */
CalendarTableView.prototype.throwAwayWeekNumberCell = function(weekNumberCell) {
    delete this._weekNumberCells[weekNumberCell.week.toString()];
    weekNumberCell.throwAway();
};

/**
 * @constructor
 * @extends View
 * @param {!Object} config
 */
function CalendarPicker(type, config) {
    View.call(this, createElement("div", CalendarPicker.ClassNameCalendarPicker));
    this.element.classList.add(CalendarPicker.ClassNamePreparing);

    /**
     * @type {!string}
     * @const
     */
    this.type = type;
    if (this.type === "week")
        this._dateTypeConstructor = Week;
    else if (this.type === "month")
        this._dateTypeConstructor = Month;
    else
        this._dateTypeConstructor = Day;
    /**
     * @type {!Object}
     * @const
     */
    this.config = {};
    this._setConfig(config);
    /**
     * @type {!Month}
     * @const
     */
    this.minimumMonth = Month.createFromDay(this.config.minimum.firstDay());
    /**
     * @type {!Month}
     * @const
     */
    this.maximumMonth = Month.createFromDay(this.config.maximum.lastDay());
    if (global.params.isLocaleRTL)
        this.element.classList.add("rtl");
    /**
     * @type {!CalendarTableView}
     * @const
     */
    this.calendarTableView = new CalendarTableView(this);
    this.calendarTableView.hasNumberColumn = this.type === "week";
    /**
     * @type {!CalendarHeaderView}
     * @const
     */
    this.calendarHeaderView = new CalendarHeaderView(this);
    this.calendarHeaderView.monthPopupButton.on(MonthPopupButton.EventTypeButtonClick, this.onMonthPopupButtonClick);
    /**
     * @type {!MonthPopupView}
     * @const
     */
    this.monthPopupView = new MonthPopupView(this.minimumMonth, this.maximumMonth);
    this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidSelectMonth, this.onYearListViewDidSelectMonth);
    this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidHide, this.onYearListViewDidHide);
    this.calendarHeaderView.attachTo(this);
    this.calendarTableView.attachTo(this);
    /**
     * @type {!Month}
     * @protected
     */
    this._currentMonth = new Month(NaN, NaN);
    /**
     * @type {?DateType}
     * @protected
     */
    this._selection = null;
    /**
     * @type {?DateType}
     * @protected
     */
    this._highlight = null;
    this.calendarTableView.element.addEventListener("keydown", this.onCalendarTableKeyDown, false);
    document.body.addEventListener("keydown", this.onBodyKeyDown, false);

    window.addEventListener("resize", this.onWindowResize, false);

    /**
     * @type {!number}
     * @protected
     */
    this._height = -1;

    var initialSelection = parseDateString(config.currentValue);
    if (initialSelection) {
        this.setCurrentMonth(Month.createFromDay(initialSelection.middleDay()), CalendarPicker.NavigationBehavior.None);
        this.setSelection(initialSelection);
    } else
        this.setCurrentMonth(Month.createFromToday(), CalendarPicker.NavigationBehavior.None);
}

CalendarPicker.prototype = Object.create(View.prototype);

CalendarPicker.Padding = 10;
CalendarPicker.BorderWidth = 1;
CalendarPicker.ClassNameCalendarPicker = "calendar-picker";
CalendarPicker.ClassNamePreparing = "preparing";
CalendarPicker.EventTypeCurrentMonthChanged = "currentMonthChanged";

/**
 * @param {!Event} event
 */
CalendarPicker.prototype.onWindowResize = function(event) {
    this.element.classList.remove(CalendarPicker.ClassNamePreparing);
    window.removeEventListener("resize", this.onWindowResize, false);
};

/**
 * @param {!YearListView} sender
 */
CalendarPicker.prototype.onYearListViewDidHide = function(sender) {
    this.monthPopupView.hide();
    this.calendarHeaderView.setDisabled(false);
    this.adjustHeight();
};

/**
 * @param {!YearListView} sender
 * @param {!Month} month
 */
CalendarPicker.prototype.onYearListViewDidSelectMonth = function(sender, month) {
    this.setCurrentMonth(month, CalendarPicker.NavigationBehavior.None);
};

/**
 * @param {!View|Node} parent
 * @param {?View|Node=} before
 * @override
 */
CalendarPicker.prototype.attachTo = function(parent, before) {
    View.prototype.attachTo.call(this, parent, before);
    this.calendarTableView.element.focus();
};

CalendarPicker.prototype.cleanup = function() {
    window.removeEventListener("resize", this.onWindowResize, false);
    this.calendarTableView.element.removeEventListener("keydown", this.onBodyKeyDown, false);
    // Month popup view might be attached to document.body.
    this.monthPopupView.hide();
};

/**
 * @param {?MonthPopupButton} sender
 */
CalendarPicker.prototype.onMonthPopupButtonClick = function(sender) {
    var clientRect = this.calendarTableView.element.getBoundingClientRect();
    var calendarTableRect = new Rectangle(clientRect.left + document.body.scrollLeft, clientRect.top + document.body.scrollTop, clientRect.width, clientRect.height);
    this.monthPopupView.show(this.currentMonth(), calendarTableRect);
    this.calendarHeaderView.setDisabled(true);
    this.adjustHeight();
};

CalendarPicker.prototype._setConfig = function(config) {
    this.config.minimum = (typeof config.min !== "undefined" && config.min) ? parseDateString(config.min) : this._dateTypeConstructor.Minimum;
    this.config.maximum = (typeof config.max !== "undefined" && config.max) ? parseDateString(config.max) : this._dateTypeConstructor.Maximum;
    this.config.minimumValue = this.config.minimum.valueOf();
    this.config.maximumValue = this.config.maximum.valueOf();
    this.config.step = (typeof config.step !== undefined) ? Number(config.step) : this._dateTypeConstructor.DefaultStep;
    this.config.stepBase = (typeof config.stepBase !== "undefined") ? Number(config.stepBase) : this._dateTypeConstructor.DefaultStepBase;
};

/**
 * @return {!Month}
 */
CalendarPicker.prototype.currentMonth = function() {
    return this._currentMonth;
};

/**
 * @enum {number}
 */
CalendarPicker.NavigationBehavior = {
    None: 0,
    WithAnimation: 1
};

/**
 * @param {!Month} month
 * @param {!CalendarPicker.NavigationBehavior} animate
 */
CalendarPicker.prototype.setCurrentMonth = function(month, behavior) {
    if (month > this.maximumMonth)
        month = this.maximumMonth;
    else if (month < this.minimumMonth)
        month = this.minimumMonth;
    if (this._currentMonth.equals(month))
        return;
    this._currentMonth = month;
    this.calendarTableView.scrollToMonth(this._currentMonth, behavior === CalendarPicker.NavigationBehavior.WithAnimation);
    this.adjustHeight();
    this.calendarTableView.setNeedsUpdateCells(true);
    this.dispatchEvent(CalendarPicker.EventTypeCurrentMonthChanged, {
        target: this
    });
};

CalendarPicker.prototype.adjustHeight = function() {
    var rowForFirstDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.firstDay()).row;
    var rowForLastDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.lastDay()).row;
    var numberOfRows = rowForLastDayInMonth - rowForFirstDayInMonth + 1;
    var calendarTableViewHeight = CalendarTableHeaderView.Height + numberOfRows * DayCell.Height + CalendarTableView.BorderWidth * 2;
    var height = (this.monthPopupView.isVisible ? YearListView.Height : calendarTableViewHeight) + CalendarHeaderView.Height + CalendarHeaderView.BottomMargin + CalendarPicker.Padding * 2 + CalendarPicker.BorderWidth * 2;
    this.setHeight(height);
};

CalendarPicker.prototype.selection = function() {
    return this._selection;
};

CalendarPicker.prototype.highlight = function() {
    return this._highlight;
};

/**
 * @return {!Day}
 */
CalendarPicker.prototype.firstVisibleDay = function() {
    var firstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
    var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow);
    if (!firstVisibleDay)
        firstVisibleDay = Day.Minimum;
    return firstVisibleDay;
};

/**
 * @return {!Day}
 */
CalendarPicker.prototype.lastVisibleDay = function() { 
    var lastVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().lastDay()).row;
    var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow);
    if (!lastVisibleDay)
        lastVisibleDay = Day.Maximum;
    return lastVisibleDay;
};

/**
 * @param {?Day} day
 */
CalendarPicker.prototype.selectRangeContainingDay = function(day) {
    var selection = day ? this._dateTypeConstructor.createFromDay(day) : null;
    this.setSelection(selection);
};

/**
 * @param {?Day} day
 */
CalendarPicker.prototype.highlightRangeContainingDay = function(day) {
    var highlight = day ? this._dateTypeConstructor.createFromDay(day) : null;
    this._setHighlight(highlight);
};

/**
 * @param {?DateType} dayOrWeekOrMonth
 */
CalendarPicker.prototype.setSelection = function(dayOrWeekOrMonth) {
    if (!this._selection && !dayOrWeekOrMonth)
        return;
    if (this._selection && this._selection.equals(dayOrWeekOrMonth))
        return;
    var firstDayInSelection = dayOrWeekOrMonth.firstDay();    
    var lastDayInSelection = dayOrWeekOrMonth.lastDay();
    var candidateCurrentMonth = Month.createFromDay(firstDayInSelection);
    if (this.firstVisibleDay() > lastDayInSelection || this.lastVisibleDay() < firstDayInSelection) {
        // Change current month if the selection is not visible at all.
        this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation);
    } else if (this.firstVisibleDay() < firstDayInSelection || this.lastVisibleDay() > lastDayInSelection) {
        // If the selection is partly visible, only change the current month if
        // doing so will make the whole selection visible.
        var firstVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.firstDay()).row;
        var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow);
        var lastVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.lastDay()).row;
        var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow);
        if (firstDayInSelection >= firstVisibleDay && lastDayInSelection <= lastVisibleDay)
            this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation);
    }
    this._setHighlight(dayOrWeekOrMonth);
    if (!this.isValid(dayOrWeekOrMonth))
        return;
    this._selection = dayOrWeekOrMonth;
    this.calendarTableView.setNeedsUpdateCells(true);
    window.pagePopupController.setValue(this._selection.toString());
};

/**
 * @param {?DateType} dayOrWeekOrMonth
 */
CalendarPicker.prototype._setHighlight = function(dayOrWeekOrMonth) {
    if (!this._highlight && !dayOrWeekOrMonth)
        return;
    if (!dayOrWeekOrMonth && !this._highlight)
        return;
    if (this._highlight && this._highlight.equals(dayOrWeekOrMonth))
        return;
    this._highlight = dayOrWeekOrMonth;
    this.calendarTableView.setNeedsUpdateCells(true);
};

/**
 * @param {!number} value
 * @return {!boolean}
 */
CalendarPicker.prototype._stepMismatch = function(value) {
    var nextAllowedValue = Math.ceil((value - this.config.stepBase) / this.config.step) * this.config.step + this.config.stepBase;
    return nextAllowedValue >= value + this._dateTypeConstructor.DefaultStep;
};

/**
 * @param {!number} value
 * @return {!boolean}
 */
CalendarPicker.prototype._outOfRange = function(value) {
    return value < this.config.minimumValue || value > this.config.maximumValue;
};

/**
 * @param {!DateType} dayOrWeekOrMonth
 * @return {!boolean}
 */
CalendarPicker.prototype.isValid = function(dayOrWeekOrMonth) {
    var value = dayOrWeekOrMonth.valueOf();
    return dayOrWeekOrMonth instanceof this._dateTypeConstructor && !this._outOfRange(value) && !this._stepMismatch(value);
};

/**
 * @param {!Day} day
 * @return {!boolean}
 */
CalendarPicker.prototype.isValidDay = function(day) {
    return this.isValid(this._dateTypeConstructor.createFromDay(day));
};

/**
 * @param {!DateType} dateRange
 * @return {!boolean} Returns true if the highlight was changed.
 */
CalendarPicker.prototype._moveHighlight = function(dateRange) {
    if (!dateRange)
        return false;
    if (this._outOfRange(dateRange.valueOf()))
        return false;
    if (this.firstVisibleDay() > dateRange.middleDay() || this.lastVisibleDay() < dateRange.middleDay())
        this.setCurrentMonth(Month.createFromDay(dateRange.middleDay()), CalendarPicker.NavigationBehavior.WithAnimation);
    this._setHighlight(dateRange);
    return true;
};

/**
 * @param {?Event} event
 */
CalendarPicker.prototype.onCalendarTableKeyDown = function(event) {
    var key = event.keyIdentifier;
    var eventHandled = false;
    if (key == "U+0054") { // 't' key.
        this.selectRangeContainingDay(Day.createFromToday());
        eventHandled = true;
    } else if (key == "PageUp") {
        var previousMonth = this.currentMonth().previous();
        if (previousMonth && previousMonth >= this.config.minimumValue) {
            this.setCurrentMonth(previousMonth, CalendarPicker.NavigationBehavior.WithAnimation);
            eventHandled = true;
        }
    } else if (key == "PageDown") {
        var nextMonth = this.currentMonth().next();
        if (nextMonth && nextMonth >= this.config.minimumValue) {
            this.setCurrentMonth(nextMonth, CalendarPicker.NavigationBehavior.WithAnimation);
            eventHandled = true;
        }
    } else if (this._highlight) {
        if (global.params.isLocaleRTL ? key == "Right" : key == "Left") {
            eventHandled = this._moveHighlight(this._highlight.previous());
        } else if (key == "Up") {
            eventHandled = this._moveHighlight(this._highlight.previous(this.type === "date" ? DaysPerWeek : 1));
        } else if (global.params.isLocaleRTL ? key == "Left" : key == "Right") {
            eventHandled = this._moveHighlight(this._highlight.next());
        } else if (key == "Down") {
            eventHandled = this._moveHighlight(this._highlight.next(this.type === "date" ? DaysPerWeek : 1));
        } else if (key == "Enter") {
            this.setSelection(this._highlight);
        }
    } else if (key == "Left" || key == "Up" || key == "Right" || key == "Down") {
        // Highlight range near the middle.
        this.highlightRangeContainingDay(this.currentMonth().middleDay());
        eventHandled = true;
    }

    if (eventHandled) {
        event.stopPropagation();
        event.preventDefault();
    }
};

/**
 * @return {!number} Width in pixels.
 */
CalendarPicker.prototype.width = function() {
    return this.calendarTableView.width() + (CalendarTableView.BorderWidth + CalendarPicker.BorderWidth + CalendarPicker.Padding) * 2;
};

/**
 * @return {!number} Height in pixels.
 */
CalendarPicker.prototype.height = function() {
    return this._height;
};

/**
 * @param {!number} height Height in pixels.
 */
CalendarPicker.prototype.setHeight = function(height) {
    if (this._height === height)
        return;
    this._height = height;
    resizeWindow(this.width(), this._height);
    this.calendarTableView.setHeight(this._height - CalendarHeaderView.Height - CalendarHeaderView.BottomMargin - CalendarPicker.Padding * 2 - CalendarTableView.BorderWidth * 2);
};

/**
 * @param {?Event} event
 */
CalendarPicker.prototype.onBodyKeyDown = function(event) {
    var key = event.keyIdentifier;
    var eventHandled = false;
    var offset = 0;
    switch (key) {
    case "U+001B": // Esc key.
        window.pagePopupController.closePopup();
        eventHandled = true;
        break;
    case "U+004D": // 'm' key.
        offset = offset || 1; // Fall-through.
    case "U+0059": // 'y' key.
        offset = offset || MonthsPerYear; // Fall-through.
    case "U+0044": // 'd' key.
        offset = offset || MonthsPerYear * 10;
        var oldFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
        this.setCurrentMonth(event.shiftKey ? this.currentMonth().previous(offset) : this.currentMonth().next(offset), CalendarPicker.NavigationBehavior.WithAnimation);
        var newFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
        if (this._highlight) {
            var highlightMiddleDay = this._highlight.middleDay();
            this.highlightRangeContainingDay(highlightMiddleDay.next((newFirstVisibleRow - oldFirstVisibleRow) * DaysPerWeek));
        }
        eventHandled  =true;
        break;
    }
    if (eventHandled) {
        event.stopPropagation();
        event.preventDefault();
    }
}

if (window.dialogArguments) {
    initialize(dialogArguments);
} else {
    window.addEventListener("message", handleMessage, false);
}
