/*
 * Copyright (c) 2013 - present Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

define(function (require, exports, module) {
    "use strict";

    var ScopeManager = brackets.getModule("JSUtils/ScopeManager"),
        OVERWRITE_EXISTING_HINT = false;

    function JSParameterHintsProvider() {
        this.hintState = {};
        this.hintStack = [];
        this.preserveHintStack = null; // close a function hint without clearing stack
        this.session = null; // current editor session, updated by main
    }

    /**
     * Update the current session for use by the Function Hint Manager.
     *
     * @param {Session} value - current session.
     */
    JSParameterHintsProvider.prototype.setSession = function (value) {
        this.session = value;
    };

    /**
     * Test if a function hint is being displayed.
     *
     * @return {boolean} - true if a function hint is being displayed, false
     * otherwise.
     */
    JSParameterHintsProvider.prototype.isHintDisplayed = function () {
        return this.hintState.visible === true;
    };

    /**
     * Save the state of the current hint. Called when popping up a parameter hint
     * for a parameter, when the parameter already part of an existing parameter
     * hint.
     */
    JSParameterHintsProvider.prototype.pushHintOnStack = function () {
        this.hintStack.push(this.hintState);
    };

    /**
     * Restore the state of the previous function hint.
     *
     * @return {boolean} - true the a parameter hint has been popped, false otherwise.
     */
    JSParameterHintsProvider.prototype.popHintFromStack = function () {
        if (this.hintStack.length > 0) {
            this.hintState = this.hintStack.pop();
            this.hintState.visible = false;
            return true;
        }

        return false;
    };

    /**
     * Reset the function hint stack.
     */
    JSParameterHintsProvider.prototype.clearFunctionHintStack = function () {
        this.hintStack = [];
    };

    /**
     * Test if the function call at the cursor is different from the currently displayed
     * function hint.
     *
     * @param {{line:number, ch:number}} functionCallPos - the offset of the function call.
     * @return {boolean}
     */
    JSParameterHintsProvider.prototype.hasFunctionCallPosChanged = function (functionCallPos) {
        var oldFunctionCallPos = this.hintState.functionCallPos;
        return (oldFunctionCallPos === undefined ||
            oldFunctionCallPos.line !== functionCallPos.line ||
            oldFunctionCallPos.ch !== functionCallPos.ch);
    };

    /**
     * Dismiss the function hint.
     *
     */
    JSParameterHintsProvider.prototype.cleanHintState = function () {
        if (this.hintState.visible) {
            if (!this.preserveHintStack) {
                this.clearFunctionHintStack();
            }
        }
    };

    /**
     * Pop up a function hint on the line above the caret position.
     *
     * @param {boolean=} pushExistingHint - if true, push the existing hint on the stack. Default is false, not
     * to push the hint.
     * @param {string=} hint - function hint string from tern.
     * @param {{inFunctionCall: boolean, functionCallPos:
     * {line: number, ch: number}}=} functionInfo -
     * if the functionInfo is already known, it can be passed in to avoid
     * figuring it out again.
     * @return {jQuery.Promise} - The promise will not complete until the
     *      hint has completed. Returns null, if the function hint is already
     *      displayed or there is no function hint at the cursor.
     *
     */
    JSParameterHintsProvider.prototype._getParameterHint = function (pushExistingHint, hint, functionInfo) {
        var result = $.Deferred();
        functionInfo = functionInfo || this.session.getFunctionInfo();
        if (!functionInfo.inFunctionCall) {
            this.cleanHintState();
            return result.reject(null);
        }

        if (this.hasFunctionCallPosChanged(functionInfo.functionCallPos)) {

            var pushHint = pushExistingHint && this.isHintDisplayed();
            if (pushHint) {
                this.pushHintOnStack();
                this.preserveHintStack = true;
            }

            this.cleanHintState();
            this.preserveHintStack = false;
        } else if (this.isHintDisplayed()) {
            return result.reject(null);
        }

        this.hintState.functionCallPos = functionInfo.functionCallPos;

        var request = null;
        if (!hint) {
            request = ScopeManager.requestParameterHint(this.session, functionInfo.functionCallPos);
        } else {
            this.session.setFnType(hint);
            request = $.Deferred();
            request.resolveWith(null, [hint]);
        }

        var self = this;
        request.done(function (fnType) {
            var hints = self.session.getParameterHint(functionInfo.functionCallPos);
            hints.functionCallPos = functionInfo.functionCallPos;
            result.resolve(hints);
        }).fail(function () {
            self.hintState = {};
            result.reject(null);
        });

        return result;
    };

    JSParameterHintsProvider.prototype.hasParameterHints = function () {
        var functionInfo = this.session.getFunctionInfo();

        return functionInfo.inFunctionCall;
    };

    JSParameterHintsProvider.prototype.getParameterHints = function (explicit, onCursorActivity) {
        var functionInfo = this.session.getFunctionInfo(),
            result = null;

        if (!onCursorActivity) {
            if (functionInfo.inFunctionCall) {
                var token = this.session.getToken();

                if ((token && token.string === "(") || explicit) {
                    return this._getParameterHint();
                }
            } else {
                this.cleanHintState();
            }

            return $.Deferred().reject(null);
        }

        if (!functionInfo.inFunctionCall) {
            this.cleanHintState();
            return $.Deferred().reject(null);
        }

        // If in a different function hint, then dismiss the old one and
        // display the new one if there is one on the stack
        if (this.hasFunctionCallPosChanged(functionInfo.functionCallPos)) {
            if (this.popHintFromStack()) {
                var poppedFunctionCallPos = this.hintState.functionCallPos,
                    currentFunctionCallPos = this.functionInfo.functionCallPos;

                if (poppedFunctionCallPos.line === currentFunctionCallPos.line &&
                        poppedFunctionCallPos.ch === currentFunctionCallPos.ch) {
                    this.preserveHintStack = true;
                    result = this._getParameterHint(OVERWRITE_EXISTING_HINT,
                        this.hintState.fnType, functionInfo);
                    this.preserveHintStack = false;
                    return result;
                }
            } else {
                this.cleanHintState();
            }
        }

        var hints = this.session.getParameterHint(functionInfo.functionCallPos);
        hints.functionCallPos = functionInfo.functionCallPos;
        return $.Deferred().resolve(hints);
    };

    exports.JSParameterHintsProvider = JSParameterHintsProvider;
});
