"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:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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.
 */

/**
 * @param {!Element} element
 * @param {!Object} config
 */
function SuggestionPicker(element, config) {
    Picker.call(this, element, config);
    this._isFocusByMouse = false;
    this._containerElement = null;
    this._setColors();
    this._layout();
    this._fixWindowSize();
    this._handleBodyKeyDownBound = this._handleBodyKeyDown.bind(this);
    document.body.addEventListener("keydown", this._handleBodyKeyDownBound);
    this._element.addEventListener("mouseout", this._handleMouseOut.bind(this), false);
};
SuggestionPicker.prototype = Object.create(Picker.prototype);

SuggestionPicker.NumberOfVisibleEntries = 20;

// An entry needs to be at least this many pixels visible for it to be a visible entry.
SuggestionPicker.VisibleEntryThresholdHeight = 4;

SuggestionPicker.ActionNames = {
    OpenCalendarPicker: "openCalendarPicker"
};

SuggestionPicker.ListEntryClass = "suggestion-list-entry";

SuggestionPicker.validateConfig = function(config) {
    if (config.showOtherDateEntry && !config.otherDateLabel)
        return "No otherDateLabel.";
    if (config.suggestionHighlightColor && !config.suggestionHighlightColor)
        return "No suggestionHighlightColor.";
    if (config.suggestionHighlightTextColor && !config.suggestionHighlightTextColor)
        return "No suggestionHighlightTextColor.";
    if (config.suggestionValues.length !== config.localizedSuggestionValues.length)
        return "localizedSuggestionValues.length must equal suggestionValues.length.";
    if (config.suggestionValues.length !== config.suggestionLabels.length)
        return "suggestionLabels.length must equal suggestionValues.length.";
    if (typeof config.inputWidth === "undefined")
        return "No inputWidth.";
    return null;
};

SuggestionPicker.prototype._setColors = function() {
    var text = "." + SuggestionPicker.ListEntryClass + ":focus {\
        background-color: " + this._config.suggestionHighlightColor + ";\
        color: " + this._config.suggestionHighlightTextColor + "; }";
    text += "." + SuggestionPicker.ListEntryClass + ":focus .label { color: " + this._config.suggestionHighlightTextColor + "; }";
    document.head.appendChild(createElement("style", null, text));
};

SuggestionPicker.prototype.cleanup = function() {
    document.body.removeEventListener("keydown", this._handleBodyKeyDownBound, false);
};

/**
 * @param {!string} title
 * @param {!string} label
 * @param {!string} value
 * @return {!Element}
 */
SuggestionPicker.prototype._createSuggestionEntryElement = function(title, label, value) {
    var entryElement = createElement("li", SuggestionPicker.ListEntryClass);
    entryElement.tabIndex = 0;
    entryElement.dataset.value = value;
    var content = createElement("span", "content");
    entryElement.appendChild(content);
    var titleElement = createElement("span", "title", title);
    content.appendChild(titleElement);
    if (label) {
        var labelElement = createElement("span", "label", label);
        content.appendChild(labelElement);
    }
    entryElement.addEventListener("mouseover", this._handleEntryMouseOver.bind(this), false);
    return entryElement;
};

/**
 * @param {!string} title
 * @param {!string} actionName
 * @return {!Element}
 */
SuggestionPicker.prototype._createActionEntryElement = function(title, actionName) {
    var entryElement = createElement("li", SuggestionPicker.ListEntryClass);
    entryElement.tabIndex = 0;
    entryElement.dataset.action = actionName;
    var content = createElement("span", "content");
    entryElement.appendChild(content);
    var titleElement = createElement("span", "title", title);
    content.appendChild(titleElement);
    entryElement.addEventListener("mouseover", this._handleEntryMouseOver.bind(this), false);
    return entryElement;
};

/**
* @return {!number}
*/
SuggestionPicker.prototype._measureMaxContentWidth = function() {
    // To measure the required width, we first set the class to "measuring-width" which
    // left aligns all the content including label.
    this._containerElement.classList.add("measuring-width");
    var maxContentWidth = 0;
    var contentElements = this._containerElement.getElementsByClassName("content");
    for (var i=0; i < contentElements.length; ++i) {
        maxContentWidth = Math.max(maxContentWidth, contentElements[i].offsetWidth);
    }
    this._containerElement.classList.remove("measuring-width");
    return maxContentWidth;
};

SuggestionPicker.prototype._fixWindowSize = function() {
    var ListBorder = 2;
    var desiredWindowWidth = this._measureMaxContentWidth() + ListBorder;
    if (typeof this._config.inputWidth === "number")
        desiredWindowWidth = Math.max(this._config.inputWidth, desiredWindowWidth);
    var totalHeight = ListBorder;
    var maxHeight = 0;
    var entryCount = 0;
    for (var i = 0; i < this._containerElement.childNodes.length; ++i) {
        var node = this._containerElement.childNodes[i];
        if (node.classList.contains(SuggestionPicker.ListEntryClass))
            entryCount++;
        totalHeight += node.offsetHeight;
        if (maxHeight === 0 && entryCount == SuggestionPicker.NumberOfVisibleEntries)
            maxHeight = totalHeight;
    }
    var desiredWindowHeight = totalHeight;
    if (maxHeight !== 0 && totalHeight > maxHeight) {
        this._containerElement.style.maxHeight = (maxHeight - ListBorder) + "px";
        desiredWindowWidth += getScrollbarWidth();
        desiredWindowHeight = maxHeight;
    }

    resizeWindow(desiredWindowWidth, desiredWindowHeight);
};

SuggestionPicker.prototype._layout = function() {
    if (this._config.isRTL)
        this._element.classList.add("rtl");
    this._containerElement = createElement("ul", "suggestion-list");
    this._containerElement.addEventListener("click", this._handleEntryClick.bind(this), false);
    for (var i = 0; i < this._config.suggestionValues.length; ++i) {
        this._containerElement.appendChild(this._createSuggestionEntryElement(this._config.localizedSuggestionValues[i], this._config.suggestionLabels[i], this._config.suggestionValues[i]));
    }
    if (this._config.showOtherDateEntry) {
        // Add separator
        var separator = createElement("div", "separator");
        this._containerElement.appendChild(separator);

        // Add "Other..." entry
        var otherEntry = this._createActionEntryElement(this._config.otherDateLabel, SuggestionPicker.ActionNames.OpenCalendarPicker);
        this._containerElement.appendChild(otherEntry);
    }
    this._element.appendChild(this._containerElement);
};

/**
 * @param {!Element} entry
 */
SuggestionPicker.prototype.selectEntry = function(entry) {
    if (typeof entry.dataset.value !== "undefined") {
        this.submitValue(entry.dataset.value);
    } else if (entry.dataset.action === SuggestionPicker.ActionNames.OpenCalendarPicker) {
        openCalendarPicker();
    }
};

/**
 * @param {!Event} event
 */
SuggestionPicker.prototype._handleEntryClick = function(event) {
    var entry = enclosingNodeOrSelfWithClass(event.target, SuggestionPicker.ListEntryClass);
    if (!entry)
        return;
    this.selectEntry(entry);
    event.preventDefault();
};

/**
 * @return {?Element}
 */
SuggestionPicker.prototype._findFirstVisibleEntry = function() {
    var scrollTop = this._containerElement.scrollTop;
    var childNodes = this._containerElement.childNodes;
    for (var i = 0; i < childNodes.length; ++i) {
        var node = childNodes[i];
        if (node.nodeType !== Node.ELEMENT_NODE || !node.classList.contains(SuggestionPicker.ListEntryClass))
            continue;
        if (node.offsetTop + node.offsetHeight - scrollTop > SuggestionPicker.VisibleEntryThresholdHeight)
            return node;
    }
    return null;
};

/**
 * @return {?Element}
 */
SuggestionPicker.prototype._findLastVisibleEntry = function() {
    var scrollBottom = this._containerElement.scrollTop + this._containerElement.offsetHeight;
    var childNodes = this._containerElement.childNodes;
    for (var i = childNodes.length - 1; i >= 0; --i){
        var node = childNodes[i];
        if (node.nodeType !== Node.ELEMENT_NODE || !node.classList.contains(SuggestionPicker.ListEntryClass))
            continue;
        if (scrollBottom - node.offsetTop > SuggestionPicker.VisibleEntryThresholdHeight)
            return node;
    }
    return null;
};

/**
 * @param {!Event} event
 */
SuggestionPicker.prototype._handleBodyKeyDown = function(event) {
    var eventHandled = false;
    var key = event.keyIdentifier;
    if (key === "U+001B") { // ESC
        this.handleCancel();
        eventHandled = true;
    } else if (key == "Up") {
        if (document.activeElement && document.activeElement.classList.contains(SuggestionPicker.ListEntryClass)) {
            for (var node = document.activeElement.previousElementSibling; node; node = node.previousElementSibling) {
                if (node.classList.contains(SuggestionPicker.ListEntryClass)) {
                    this._isFocusByMouse = false;
                    node.focus();
                    break;
                }
            }
        } else {
            this._element.querySelector("." + SuggestionPicker.ListEntryClass + ":last-child").focus();
        }
        eventHandled = true;
    } else if (key == "Down") {
        if (document.activeElement && document.activeElement.classList.contains(SuggestionPicker.ListEntryClass)) {
            for (var node = document.activeElement.nextElementSibling; node; node = node.nextElementSibling) {
                if (node.classList.contains(SuggestionPicker.ListEntryClass)) {
                    this._isFocusByMouse = false;
                    node.focus();
                    break;
                }
            }
        } else {
            this._element.querySelector("." + SuggestionPicker.ListEntryClass + ":first-child").focus();
        }
        eventHandled = true;
    } else if (key === "Enter") {
        this.selectEntry(document.activeElement);
        eventHandled = true;
    } else if (key === "PageUp") {
        this._containerElement.scrollTop -= this._containerElement.clientHeight;
        // Scrolling causes mouseover event to be called and that tries to move the focus too.
        // To prevent flickering we won't focus if the current focus was caused by the mouse.
        if (!this._isFocusByMouse)
            this._findFirstVisibleEntry().focus();
        eventHandled = true;
    } else if (key === "PageDown") {
        this._containerElement.scrollTop += this._containerElement.clientHeight;
        if (!this._isFocusByMouse)
            this._findLastVisibleEntry().focus();
        eventHandled = true;
    }
    if (eventHandled)
        event.preventDefault();
};

/**
 * @param {!Event} event
 */
SuggestionPicker.prototype._handleEntryMouseOver = function(event) {
    var entry = enclosingNodeOrSelfWithClass(event.target, SuggestionPicker.ListEntryClass);
    if (!entry)
        return;
    this._isFocusByMouse = true;
    entry.focus();
    event.preventDefault();
};

/**
 * @param {!Event} event
 */
SuggestionPicker.prototype._handleMouseOut = function(event) {
    if (!document.activeElement.classList.contains(SuggestionPicker.ListEntryClass))
        return;
    this._isFocusByMouse = false;
    document.activeElement.blur();
    event.preventDefault();
};
