/*
 * Copyright (C) 2006, 2007, 2008 Apple Inc.  All rights reserved.
 * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
 * Copyright (C) 2009 Joseph Pecoraro
 * Copyright (C) 2011 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.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") 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 APPLE 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 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.
 */

/**
 * @constructor
 * @extends {WebInspector.VBox}
 * @param {!WebInspector.Searchable} searchable
 * @param {string=} settingName
 */
WebInspector.SearchableView = function(searchable, settingName)
{
    WebInspector.VBox.call(this, true);
    this.registerRequiredCSS("ui/searchableView.css");
    this.element[WebInspector.SearchableView._symbol] = this;

    this._searchProvider = searchable;
    this._setting = settingName ? WebInspector.settings.createSetting(settingName, {}) : null;

    this.contentElement.createChild("content");
    this._footerElementContainer = this.contentElement.createChild("div", "search-bar hidden");
    this._footerElementContainer.style.order = 100;

    var toolbar = new WebInspector.Toolbar("search-toolbar", this._footerElementContainer);

    if (this._searchProvider.supportsCaseSensitiveSearch()) {
        this._caseSensitiveButton = new WebInspector.ToolbarToggle(WebInspector.UIString("Case sensitive"), "");
        this._caseSensitiveButton.setText("Aa");
        this._caseSensitiveButton.addEventListener("click", this._toggleCaseSensitiveSearch, this);
        toolbar.appendToolbarItem(this._caseSensitiveButton);
    }

    if (this._searchProvider.supportsRegexSearch()) {
        this._regexButton = new WebInspector.ToolbarToggle(WebInspector.UIString("Regex"), "");
        this._regexButton.setText(".*");
        this._regexButton.addEventListener("click", this._toggleRegexSearch, this);
        toolbar.appendToolbarItem(this._regexButton);
    }

    this._footerElement = this._footerElementContainer.createChild("table", "toolbar-search");
    this._footerElement.cellSpacing = 0;

    this._firstRowElement = this._footerElement.createChild("tr");
    this._secondRowElement = this._footerElement.createChild("tr", "hidden");

    // Column 1
    var searchControlElementColumn = this._firstRowElement.createChild("td");
    this._searchControlElement = searchControlElementColumn.createChild("span", "toolbar-search-control");

    this._searchInputElement = WebInspector.HistoryInput.create();
    this._searchInputElement.classList.add("search-replace");
    this._searchControlElement.appendChild(this._searchInputElement);

    this._searchInputElement.id = "search-input-field";
    this._searchInputElement.placeholder = WebInspector.UIString("Find");

    this._matchesElement = this._searchControlElement.createChild("label", "search-results-matches");
    this._matchesElement.setAttribute("for", "search-input-field");

    this._searchNavigationElement = this._searchControlElement.createChild("div", "toolbar-search-navigation-controls");

    this._searchNavigationPrevElement = this._searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-prev");
    this._searchNavigationPrevElement.addEventListener("click", this._onPrevButtonSearch.bind(this), false);
    this._searchNavigationPrevElement.title = WebInspector.UIString("Search Previous");

    this._searchNavigationNextElement = this._searchNavigationElement.createChild("div", "toolbar-search-navigation toolbar-search-navigation-next");
    this._searchNavigationNextElement.addEventListener("click", this._onNextButtonSearch.bind(this), false);
    this._searchNavigationNextElement.title = WebInspector.UIString("Search Next");

    this._searchInputElement.addEventListener("mousedown", this._onSearchFieldManualFocus.bind(this), false); // when the search field is manually selected
    this._searchInputElement.addEventListener("keydown", this._onSearchKeyDown.bind(this), true);
    this._searchInputElement.addEventListener("input", this._onInput.bind(this), false);

    this._replaceInputElement = this._secondRowElement.createChild("td").createChild("input", "search-replace toolbar-replace-control");
    this._replaceInputElement.addEventListener("keydown", this._onReplaceKeyDown.bind(this), true);
    this._replaceInputElement.placeholder = WebInspector.UIString("Replace");

    // Column 2
    this._findButtonElement = this._firstRowElement.createChild("td").createChild("button", "search-action-button hidden");
    this._findButtonElement.textContent = WebInspector.UIString("Find");
    this._findButtonElement.tabIndex = -1;
    this._findButtonElement.addEventListener("click", this._onFindClick.bind(this), false);

    this._replaceButtonElement = this._secondRowElement.createChild("td").createChild("button", "search-action-button");
    this._replaceButtonElement.textContent = WebInspector.UIString("Replace");
    this._replaceButtonElement.disabled = true;
    this._replaceButtonElement.tabIndex = -1;
    this._replaceButtonElement.addEventListener("click", this._replace.bind(this), false);

    // Column 3
    this._prevButtonElement = this._firstRowElement.createChild("td").createChild("button", "search-action-button hidden");
    this._prevButtonElement.textContent = WebInspector.UIString("Previous");
    this._prevButtonElement.tabIndex = -1;
    this._prevButtonElement.addEventListener("click", this._onPreviousClick.bind(this), false);

    this._replaceAllButtonElement = this._secondRowElement.createChild("td").createChild("button", "search-action-button");
    this._replaceAllButtonElement.textContent = WebInspector.UIString("Replace All");
    this._replaceAllButtonElement.addEventListener("click", this._replaceAll.bind(this), false);

    // Column 4
    this._replaceElement = this._firstRowElement.createChild("td").createChild("span");

    this._replaceLabelElement = createCheckboxLabel(WebInspector.UIString("Replace"));
    this._replaceCheckboxElement = this._replaceLabelElement.checkboxElement;
    this._uniqueId = ++WebInspector.SearchableView._lastUniqueId;
    var replaceCheckboxId = "search-replace-trigger" + this._uniqueId;
    this._replaceCheckboxElement.id = replaceCheckboxId;
    this._replaceCheckboxElement.addEventListener("change", this._updateSecondRowVisibility.bind(this), false);

    this._replaceElement.appendChild(this._replaceLabelElement);

    // Column 5
    var cancelButtonElement = this._firstRowElement.createChild("td").createChild("button", "search-action-button");
    cancelButtonElement.textContent = WebInspector.UIString("Cancel");
    cancelButtonElement.tabIndex = -1;
    cancelButtonElement.addEventListener("click", this.closeSearch.bind(this), false);
    this._minimalSearchQuerySize = 3;

    this._loadSetting();
}

WebInspector.SearchableView._lastUniqueId = 0;

WebInspector.SearchableView._symbol = Symbol("searchableView");

/**
 * @param {?Element} element
 * @return {?WebInspector.SearchableView}
 */
WebInspector.SearchableView.fromElement = function(element)
{
    var view = null;
    while (element && !view) {
        view = element[WebInspector.SearchableView._symbol];
        element = element.parentElementOrShadowHost();
    }
    return view;
}

WebInspector.SearchableView.prototype = {
    _toggleCaseSensitiveSearch: function()
    {
        this._caseSensitiveButton.setToggled(!this._caseSensitiveButton.toggled());
        this._saveSetting();
        this._performSearch(false, true);
    },

    _toggleRegexSearch: function()
    {
        this._regexButton.setToggled(!this._regexButton.toggled());
        this._saveSetting();
        this._performSearch(false, true);
    },

    _saveSetting: function()
    {
        if (!this._setting)
            return;
        var settingValue = this._setting.get() || {};
        settingValue.caseSensitive = this._caseSensitiveButton.toggled();
        settingValue.isRegex = this._regexButton.toggled();
        this._setting.set(settingValue);
    },

    _loadSetting: function()
    {
        var settingValue = this._setting ? (this._setting.get() || {}) : {};
        if (this._searchProvider.supportsCaseSensitiveSearch())
            this._caseSensitiveButton.setToggled(!!settingValue.caseSensitive);
        if (this._searchProvider.supportsRegexSearch())
            this._regexButton.setToggled(!!settingValue.isRegex);
    },

    /**
     * @override
     * @return {!Element}
     */
    defaultFocusedElement: function()
    {
        var children = this.children();
        for (var i = 0; i < children.length; ++i) {
            var element = children[i].defaultFocusedElement();
            if (element)
                return element;
        }
        return WebInspector.Widget.prototype.defaultFocusedElement.call(this);
    },

    /**
     * @param {number} minimalSearchQuerySize
     */
    setMinimalSearchQuerySize: function(minimalSearchQuerySize)
    {
        this._minimalSearchQuerySize = minimalSearchQuerySize;
    },

    /**
     * @param {string} placeholder
     */
    setPlaceholder: function(placeholder)
    {
        this._searchInputElement.placeholder = placeholder;
    },

    /**
     * @param {boolean} replaceable
     */
    setReplaceable: function(replaceable)
    {
        this._replaceable = replaceable;
    },

    /**
     * @param {number} matches
     */
    updateSearchMatchesCount: function(matches)
    {
        this._searchProvider.currentSearchMatches = matches;
        this._updateSearchMatchesCountAndCurrentMatchIndex(this._searchProvider.currentQuery ? matches : 0, -1);
    },

    /**
     * @param {number} currentMatchIndex
     */
    updateCurrentMatchIndex: function(currentMatchIndex)
    {
        this._updateSearchMatchesCountAndCurrentMatchIndex(this._searchProvider.currentSearchMatches, currentMatchIndex);
    },

    /**
     * @return {boolean}
     */
    isSearchVisible: function()
    {
        return this._searchIsVisible;
    },

    closeSearch: function()
    {
        this.cancelSearch();
        if (WebInspector.currentFocusElement() && WebInspector.currentFocusElement().isDescendant(this._footerElementContainer))
            this.focus();
    },

    _toggleSearchBar: function(toggled)
    {
        this._footerElementContainer.classList.toggle("hidden", !toggled);
        this.doResize();
    },

    cancelSearch: function()
    {
        if (!this._searchIsVisible)
            return;
        this.resetSearch();
        delete this._searchIsVisible;
        this._toggleSearchBar(false);
    },

    resetSearch: function()
    {
        this._clearSearch();
        this._updateReplaceVisibility();
        this._matchesElement.textContent = "";
    },

    refreshSearch: function()
    {
        if (!this._searchIsVisible)
            return;
        this.resetSearch();
        this._performSearch(false, false);
    },

    /**
     * @return {boolean}
     */
    handleFindNextShortcut: function()
    {
        if (!this._searchIsVisible)
            return false;
        this._searchProvider.jumpToNextSearchResult();
        return true;
    },

    /**
     * @return {boolean}
     */
    handleFindPreviousShortcut: function()
    {
        if (!this._searchIsVisible)
            return false;
        this._searchProvider.jumpToPreviousSearchResult();
        return true;
    },

    /**
     * @return {boolean}
     */
    handleFindShortcut: function()
    {
        this.showSearchField();
        return true;
    },

    /**
     * @return {boolean}
     */
    handleCancelSearchShortcut: function()
    {
        if (!this._searchIsVisible)
            return false;
        this.closeSearch();
        return true;
    },

    /**
     * @param {boolean} enabled
     */
    _updateSearchNavigationButtonState: function(enabled)
    {
        this._replaceButtonElement.disabled = !enabled;
        if (enabled) {
            this._searchNavigationPrevElement.classList.add("enabled");
            this._searchNavigationNextElement.classList.add("enabled");
        } else {
            this._searchNavigationPrevElement.classList.remove("enabled");
            this._searchNavigationNextElement.classList.remove("enabled");
        }
    },

    /**
     * @param {number} matches
     * @param {number} currentMatchIndex
     */
    _updateSearchMatchesCountAndCurrentMatchIndex: function(matches, currentMatchIndex)
    {
        if (!this._currentQuery)
            this._matchesElement.textContent = "";
        else if (matches === 0 || currentMatchIndex >= 0)
            this._matchesElement.textContent = WebInspector.UIString("%d of %d", currentMatchIndex + 1, matches);
        else if (matches === 1)
            this._matchesElement.textContent = WebInspector.UIString("1 match");
        else
            this._matchesElement.textContent = WebInspector.UIString("%d matches", matches);
        this._updateSearchNavigationButtonState(matches > 0);
    },

    showSearchField: function()
    {
        if (this._searchIsVisible)
            this.cancelSearch();

        var queryCandidate;
        if (WebInspector.currentFocusElement() !== this._searchInputElement) {
            var selection = this._searchInputElement.getComponentSelection();
            if (selection.rangeCount)
                queryCandidate = selection.toString().replace(/\r?\n.*/, "");
        }

        this._toggleSearchBar(true);
        this._updateReplaceVisibility();
        if (queryCandidate)
            this._searchInputElement.value = queryCandidate;
        this._performSearch(false, false);
        this._searchInputElement.focus();
        this._searchInputElement.select();
        this._searchIsVisible = true;
    },

    _updateReplaceVisibility: function()
    {
        this._replaceElement.classList.toggle("hidden", !this._replaceable);
        if (!this._replaceable) {
            this._replaceCheckboxElement.checked = false;
            this._updateSecondRowVisibility();
        }
    },

    /**
     * @param {!Event} event
     */
    _onSearchFieldManualFocus: function(event)
    {
        WebInspector.setCurrentFocusElement(/** @type {?Node} */ (event.target));
    },

    /**
     * @param {!Event} event
     */
    _onSearchKeyDown: function(event)
    {
        if (isEscKey(event)) {
            this.closeSearch();
            event.consume(true);
            return;
        }
        if (!isEnterKey(event))
            return;

        if (!this._currentQuery)
            this._performSearch(true, true, event.shiftKey);
        else
            this._jumpToNextSearchResult(event.shiftKey);
    },

    /**
     * @param {!Event} event
     */
    _onReplaceKeyDown: function(event)
    {
        if (isEnterKey(event))
            this._replace();
    },

    /**
     * @param {boolean=} isBackwardSearch
     */
    _jumpToNextSearchResult: function(isBackwardSearch)
    {
        if (!this._currentQuery || !this._searchNavigationPrevElement.classList.contains("enabled"))
            return;

        if (isBackwardSearch)
            this._searchProvider.jumpToPreviousSearchResult();
        else
            this._searchProvider.jumpToNextSearchResult();
    },

    _onNextButtonSearch: function(event)
    {
        if (!this._searchNavigationNextElement.classList.contains("enabled"))
            return;
        this._jumpToNextSearchResult();
        this._searchInputElement.focus();
    },

    _onPrevButtonSearch: function(event)
    {
        if (!this._searchNavigationPrevElement.classList.contains("enabled"))
            return;
        this._jumpToNextSearchResult(true);
        this._searchInputElement.focus();
    },

    _onFindClick: function(event)
    {
        if (!this._currentQuery)
            this._performSearch(true, true);
        else
            this._jumpToNextSearchResult();
        this._searchInputElement.focus();
    },

    _onPreviousClick: function(event)
    {
        if (!this._currentQuery)
            this._performSearch(true, true, true);
        else
            this._jumpToNextSearchResult(true);
        this._searchInputElement.focus();
    },

    _clearSearch: function()
    {
        delete this._currentQuery;
        if (!!this._searchProvider.currentQuery) {
            delete this._searchProvider.currentQuery;
            this._searchProvider.searchCanceled();
        }
        this._updateSearchMatchesCountAndCurrentMatchIndex(0, -1);
    },

    /**
     * @param {boolean} forceSearch
     * @param {boolean} shouldJump
     * @param {boolean=} jumpBackwards
     */
    _performSearch: function(forceSearch, shouldJump, jumpBackwards)
    {
        var query = this._searchInputElement.value;
        if (!query || (!forceSearch && query.length < this._minimalSearchQuerySize && !this._currentQuery)) {
            this._clearSearch();
            return;
        }

        this._currentQuery = query;
        this._searchProvider.currentQuery = query;

        var searchConfig = this._currentSearchConfig();
        this._searchProvider.performSearch(searchConfig, shouldJump, jumpBackwards);
    },

    /**
     * @return {!WebInspector.SearchableView.SearchConfig}
     */
    _currentSearchConfig: function()
    {
        var query = this._searchInputElement.value;
        var caseSensitive = this._caseSensitiveButton ? this._caseSensitiveButton.toggled() : false;
        var isRegex = this._regexButton ? this._regexButton.toggled() : false;
        return new WebInspector.SearchableView.SearchConfig(query, caseSensitive, isRegex);
    },

    _updateSecondRowVisibility: function()
    {
        var secondRowVisible = this._replaceCheckboxElement.checked;
        this._footerElementContainer.classList.toggle("replaceable", secondRowVisible);
        this._footerElement.classList.toggle("toolbar-search-replace", secondRowVisible);
        this._secondRowElement.classList.toggle("hidden", !secondRowVisible);
        this._prevButtonElement.classList.toggle("hidden", !secondRowVisible);
        this._findButtonElement.classList.toggle("hidden", !secondRowVisible);
        this._replaceCheckboxElement.tabIndex = secondRowVisible ? -1 : 0;

        if (secondRowVisible)
            this._replaceInputElement.focus();
        else
            this._searchInputElement.focus();
        this.doResize();
    },

    _replace: function()
    {
        var searchConfig = this._currentSearchConfig();
        /** @type {!WebInspector.Replaceable} */ (this._searchProvider).replaceSelectionWith(searchConfig, this._replaceInputElement.value);
        delete this._currentQuery;
        this._performSearch(true, true);
    },

    _replaceAll: function()
    {
        var searchConfig = this._currentSearchConfig();
        /** @type {!WebInspector.Replaceable} */ (this._searchProvider).replaceAllWith(searchConfig, this._replaceInputElement.value);
    },

    /**
     * @param {!Event} event
     */
    _onInput: function(event)
    {
        if (this._valueChangedTimeoutId)
            clearTimeout(this._valueChangedTimeoutId);
        var timeout = this._searchInputElement.value.length < 3 ? 200 : 0;
        this._valueChangedTimeoutId = setTimeout(this._onValueChanged.bind(this), timeout);
    },

    _onValueChanged: function()
    {
        delete this._valueChangedTimeoutId;
        this._performSearch(false, true);
    },

    __proto__: WebInspector.VBox.prototype
}

/**
 * @interface
 */
WebInspector.Searchable = function()
{
}

WebInspector.Searchable.prototype = {
    searchCanceled: function() { },

    /**
     * @param {!WebInspector.SearchableView.SearchConfig} searchConfig
     * @param {boolean} shouldJump
     * @param {boolean=} jumpBackwards
     */
    performSearch: function(searchConfig, shouldJump, jumpBackwards) { },

    jumpToNextSearchResult: function() { },

    jumpToPreviousSearchResult: function() { },

    /**
     * @return {boolean}
     */
    supportsCaseSensitiveSearch: function() { },

    /**
     * @return {boolean}
     */
    supportsRegexSearch: function() { }
}

/**
 * @interface
 */
WebInspector.Replaceable = function()
{
}

WebInspector.Replaceable.prototype = {
    /**
     * @param {!WebInspector.SearchableView.SearchConfig} searchConfig
     * @param {string} replacement
     */
    replaceSelectionWith: function(searchConfig, replacement) { },

    /**
     * @param {!WebInspector.SearchableView.SearchConfig} searchConfig
     * @param {string} replacement
     */
    replaceAllWith: function(searchConfig, replacement) { }
}

/**
 * @constructor
 * @param {string} query
 * @param {boolean} caseSensitive
 * @param {boolean} isRegex
 */
WebInspector.SearchableView.SearchConfig = function(query, caseSensitive, isRegex)
{
    this.query = query;
    this.caseSensitive = caseSensitive;
    this.isRegex = isRegex;
}

WebInspector.SearchableView.SearchConfig.prototype = {
    /**
     * @param {boolean=} global
     * @return {!RegExp}
     */
    toSearchRegex: function(global)
    {
        var modifiers = this.caseSensitive ? "" : "i";
        if (global)
            modifiers += "g";
        var query = this.isRegex ? "/" + this.query + "/" : this.query;

        var regex;

        // First try creating regex if user knows the / / hint.
        try {
            if (/^\/.+\/$/.test(query)) {
                regex = new RegExp(query.substring(1, query.length - 1), modifiers);
                regex.__fromRegExpQuery = true;
            }
        } catch (e) {
            // Silent catch.
        }

        // Otherwise just do a plain text search.
        if (!regex)
            regex = createPlainTextSearchRegex(query, modifiers);

        return regex;
    }
}
