"use strict";
import * as DevToolsUtils from 'DevTools/DevToolsUtils.js';

//todo
//if (!isWorker) {
//    loader.lazyImporter(this, "Parser", "resource://devtools/shared/Parser.jsm");
//}


// Provide an easy way to bail out of even attempting an autocompletion
// if an object has way too many properties. Protects against large objects
// with numeric values that wouldn't be tallied towards MAX_AUTOCOMPLETIONS.
export const MAX_AUTOCOMPLETE_ATTEMPTS = 100000;
// Prevent iterating over too many properties during autocomplete suggestions.
export const MAX_AUTOCOMPLETIONS = 1500;

const STATE_NORMAL = 0;
const STATE_QUOTE = 2;
const STATE_DQUOTE = 3;

const OPEN_BODY = "{[(".split("");
const CLOSE_BODY = "}])".split("");
const OPEN_CLOSE_BODY = {
    "{": "}",
    "[": "]",
    "(": ")"
};

function hasArrayIndex(str) {
    return /\[\d+\]$/.test(str);
}

/**
 * Analyses a given string to find the last statement that is interesting for
 * later completion.
 *
 * @param   string aStr
 *          A string to analyse.
 *
 * @returns object
 *          If there was an error in the string detected, then a object like
 *
 *            { err: "ErrorMesssage" }
 *
 *          is returned, otherwise a object like
 *
 *            {
 *              state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE,
 *              startPos: index of where the last statement begins
 *            }
 */
function findCompletionBeginning(aStr) {
    let bodyStack = [];

    let state = STATE_NORMAL;
    let start = 0;
    let c;
    for (let i = 0; i < aStr.length; i++) {
        c = aStr[i];

        switch (state) {
            // Normal JS state.
            case STATE_NORMAL:
                if (c == '"') {
                    state = STATE_DQUOTE;
                }
                else if (c == "'") {
                    state = STATE_QUOTE;
                }
                else if (c == ";") {
                    start = i + 1;
                }
                else if (c == " ") {
                    start = i + 1;
                }
                else if (OPEN_BODY.indexOf(c) != -1) {
                    bodyStack.push({
                        token: c,
                        start: start
                    });
                    start = i + 1;
                }
                else if (CLOSE_BODY.indexOf(c) != -1) {
                    var last = bodyStack.pop();
                    if (!last || OPEN_CLOSE_BODY[last.token] != c) {
                        return {
                            err: "syntax error"
                        };
                    }
                    if (c == "}") {
                        start = i + 1;
                    }
                    else {
                        start = last.start;
                    }
                }
                break;

            // Double quote state > " <
            case STATE_DQUOTE:
                if (c == "\\") {
                    i++;
                }
                else if (c == "\n") {
                    return {
                        err: "unterminated string literal"
                    };
                }
                else if (c == '"') {
                    state = STATE_NORMAL;
                }
                break;

            // Single quote state > ' <
            case STATE_QUOTE:
                if (c == "\\") {
                    i++;
                }
                else if (c == "\n") {
                    return {
                        err: "unterminated string literal"
                    };
                }
                else if (c == "'") {
                    state = STATE_NORMAL;
                }
                break;
        }
    }

    return {
        state: state,
        startPos: start
    };
}

/**
 * Provides a list of properties, that are possible matches based on the passed
 * Debugger.Environment/Debugger.Object and inputValue.
 *
 * @param object aDbgObject
 *        When the debugger is not paused this Debugger.Object wraps the scope for autocompletion.
 *        It is null if the debugger is paused.
 * @param object anEnvironment
 *        When the debugger is paused this Debugger.Environment is the scope for autocompletion.
 *        It is null if the debugger is not paused.
 * @param string aInputValue
 *        Value that should be completed.
 * @param number [aCursor=aInputValue.length]
 *        Optional offset in the input where the cursor is located. If this is
 *        omitted then the cursor is assumed to be at the end of the input
 *        value.
 * @returns null or object
 *          If no completion valued could be computed, null is returned,
 *          otherwise a object with the following form is returned:
 *            {
 *              matches: [ string, string, string ],
 *              matchProp: Last part of the inputValue that was used to find
 *                         the matches-strings.
 *            }
 */
export function JSPropertyProvider(aDbgObject, anEnvironment, aInputValue, aCursor) {
    if (aCursor === undefined) {
        aCursor = aInputValue.length;
    }

    let inputValue = aInputValue.substring(0, aCursor);

    // Analyse the inputValue and find the beginning of the last part that
    // should be completed.
    let beginning = findCompletionBeginning(inputValue);

    // There was an error analysing the string.
    if (beginning.err) {
        return null;
    }

    // If the current state is not STATE_NORMAL, then we are inside of an string
    // which means that no completion is possible.
    if (beginning.state != STATE_NORMAL) {
        return null;
    }

    let completionPart = inputValue.substring(beginning.startPos);
    let lastDot = completionPart.lastIndexOf(".");

    // Don't complete on just an empty string.
    if (completionPart.trim() == "") {
        return null;
    }

    // Catch literals like [1,2,3] or "foo" and return the matches from
    // their prototypes.
    // Don't run this is a worker, migrating to acorn should allow this
    // to run in a worker - Bug 1217198.
    //todo
    /*if (!isWorker && lastDot > 0) {
     let parser = new Parser();
     parser.logExceptions = false;
     let syntaxTree = parser.get(completionPart.slice(0, lastDot));
     let lastTree = syntaxTree.getLastSyntaxTree();
     let lastBody = lastTree && lastTree.AST.body[lastTree.AST.body.length - 1];

     // Finding the last expression since we've sliced up until the dot.
     // If there were parse errors this won't exist.
     if (lastBody) {
     let expression = lastBody.expression;
     let matchProp = completionPart.slice(lastDot + 1);
     if (expression.type === "ArrayExpression") {
     return getMatchedProps(Array.prototype, matchProp);
     } else if (expression.type === "Literal" &&
     (typeof expression.value === "string")) {
     return getMatchedProps(String.prototype, matchProp);
     }
     }
    }*/

    // We are completing a variable / a property lookup.
    let properties = completionPart.split(".");
    let matchProp = properties.pop().trimLeft();
    let obj = aDbgObject;

    // The first property must be found in the environment of the paused debugger
    // or of the global lexical scope.
    let env = anEnvironment || obj.asEnvironment();

    if (properties.length === 0) {
        return getMatchedPropsInEnvironment(env, matchProp);
    }

    let firstProp = properties.shift().trim();
    if (firstProp === "this") {
        // Special case for 'this' - try to get the Object from the Environment.
        // No problem if it throws, we will just not autocomplete.
        try {
            obj = env.object;
        } catch (e) {
        }
    }
    else if (hasArrayIndex(firstProp)) {
        obj = getArrayMemberProperty(null, env, firstProp);
    } else {
        obj = getVariableInEnvironment(env, firstProp);
    }

    if (!isObjectUsable(obj)) {
        return null;
    }

    // We get the rest of the properties recursively starting from the Debugger.Object
    // that wraps the first property
    for (let i = 0; i < properties.length; i++) {
        let prop = properties[i].trim();
        if (!prop) {
            return null;
        }

        if (hasArrayIndex(prop)) {
            // The property to autocomplete is a member of array. For example
            // list[i][j]..[n]. Traverse the array to get the actual element.
            obj = getArrayMemberProperty(obj, null, prop);
        }
        else {
            obj = DevToolsUtils.getProperty(obj, prop);
        }

        if (!isObjectUsable(obj)) {
            return null;
        }
    }

    // If the final property is a primitive
    if (typeof obj != "object") {
        return getMatchedProps(obj, matchProp);
    }

    return getMatchedPropsInDbgObject(obj, matchProp);
}

/**
 * Get the array member of aObj for the given aProp. For example, given
 * aProp='list[0][1]' the element at [0][1] of aObj.list is returned.
 *
 * @param object aObj
 *        The object to operate on. Should be null if aEnv is passed.
 * @param object aEnv
 *        The Environment to operate in. Should be null if aObj is passed.
 * @param string aProp
 *        The property to return.
 * @return null or Object
 *         Returns null if the property couldn't be located. Otherwise the array
 *         member identified by aProp.
 */
function getArrayMemberProperty(aObj, aEnv, aProp) {
    // First get the array.
    let obj = aObj;
    let propWithoutIndices = aProp.substr(0, aProp.indexOf("["));

    if (aEnv) {
        obj = getVariableInEnvironment(aEnv, propWithoutIndices);
    } else {
        obj = DevToolsUtils.getProperty(obj, propWithoutIndices);
    }

    if (!isObjectUsable(obj)) {
        return null;
    }

    // Then traverse the list of indices to get the actual element.
    let result;
    let arrayIndicesRegex = /\[[^\]]*\]/g;
    while ((result = arrayIndicesRegex.exec(aProp)) !== null) {
        let indexWithBrackets = result[0];
        let indexAsText = indexWithBrackets.substr(1, indexWithBrackets.length - 2);
        let index = parseInt(indexAsText);

        if (isNaN(index)) {
            return null;
        }

        obj = DevToolsUtils.getProperty(obj, index);

        if (!isObjectUsable(obj)) {
            return null;
        }
    }

    return obj;
}

/**
 * Check if the given Debugger.Object can be used for autocomplete.
 *
 * @param Debugger.Object aObject
 *        The Debugger.Object to check.
 * @return boolean
 *         True if further inspection into the object is possible, or false
 *         otherwise.
 */
function isObjectUsable(aObject) {
    if (aObject == null) {
        return false;
    }

    if (typeof aObject == "object" && aObject.class == "DeadObject") {
        return false;
    }

    return true;
}

/**
 * @see getExactMatch_impl()
 */
function getVariableInEnvironment(anEnvironment, aName) {
    return getExactMatch_impl(anEnvironment, aName, DebuggerEnvironmentSupport);
}

/**
 * @see getMatchedProps_impl()
 */
function getMatchedPropsInEnvironment(anEnvironment, aMatch) {
    return getMatchedProps_impl(anEnvironment, aMatch, DebuggerEnvironmentSupport);
}

/**
 * @see getMatchedProps_impl()
 */
function getMatchedPropsInDbgObject(aDbgObject, aMatch) {
    return getMatchedProps_impl(aDbgObject, aMatch, DebuggerObjectSupport);
}

/**
 * @see getMatchedProps_impl()
 */
function getMatchedProps(aObj, aMatch) {
    if (typeof aObj != "object") {
        aObj = aObj.constructor.prototype;
    }
    return getMatchedProps_impl(aObj, aMatch, JSObjectSupport);
}

/**
 * Get all properties in the given object (and its parent prototype chain) that
 * match a given prefix.
 *
 * @param mixed aObj
 *        Object whose properties we want to filter.
 * @param string aMatch
 *        Filter for properties that match this string.
 * @return object
 *         Object that contains the matchProp and the list of names.
 */
function getMatchedProps_impl(aObj, aMatch, {chainIterator, getProperties}) {
    let matches = new Set();
    let numProps = 0;

    // We need to go up the prototype chain.
    let iter = chainIterator(aObj);
    for (let obj of iter) {
        let props = getProperties(obj);
        numProps += props.length;

        // If there are too many properties to event attempt autocompletion,
        // or if we have already added the max number, then stop looping
        // and return the partial set that has already been discovered.
        if (numProps >= MAX_AUTOCOMPLETE_ATTEMPTS ||
            matches.size >= MAX_AUTOCOMPLETIONS) {
            break;
        }

        for (let i = 0; i < props.length; i++) {
            let prop = props[i];
            if (prop.indexOf(aMatch) != 0) {
                continue;
            }
            if (prop.indexOf('-') > -1) {
                continue;
            }
            // If it is an array index, we can't take it.
            // This uses a trick: converting a string to a number yields NaN if
            // the operation failed, and NaN is not equal to itself.
            if (+prop != +prop) {
                matches.add(prop);
            }

            if (matches.size >= MAX_AUTOCOMPLETIONS) {
                break;
            }
        }
    }

    return {
        matchProp: aMatch,
        matches: [...matches],
    };
}

/**
 * Returns a property value based on its name from the given object, by
 * recursively checking the object's prototype.
 *
 * @param object aObj
 *        An object to look the property into.
 * @param string aName
 *        The property that is looked up.
 * @returns object|undefined
 *        A Debugger.Object if the property exists in the object's prototype
 *        chain, undefined otherwise.
 */
function getExactMatch_impl(aObj, aName, {chainIterator, getProperty}) {
    // We need to go up the prototype chain.
    let iter = chainIterator(aObj);
    for (let obj of iter) {
        let prop = getProperty(obj, aName, aObj);
        if (prop) {
            return prop.value;
        }
    }
    return undefined;
}


var JSObjectSupport = {
    chainIterator: function*(aObj) {
        while (aObj) {
            yield aObj;
            aObj = Object.getPrototypeOf(aObj);
        }
    },

    getProperties: function (aObj) {
        return Object.getOwnPropertyNames(aObj);
    },

    getProperty: function () {
        // getProperty is unsafe with raw JS objects.
        throw "Unimplemented!";
    },
};

var DebuggerObjectSupport = {
    chainIterator: function*(aObj) {
        while (aObj) {
            yield aObj;
            aObj = aObj.proto;
        }
    },

    getProperties: function (aObj) {
        return aObj.getOwnPropertyNames();
    },

    getProperty: function (aObj, aName, aRootObj) {
        // This is left unimplemented in favor to DevToolsUtils.getProperty().
        throw "Unimplemented!";
    },
};

var DebuggerEnvironmentSupport = {
    chainIterator: function*(aObj) {
        while (aObj) {
            yield aObj;
            aObj = aObj.parent;
        }
    },

    getProperties: function (aObj) {
        let names = aObj.names();

        // Include 'this' in results (in sorted order)
        for (let i = 0; i < names.length; i++) {
            if (i === names.length - 1 || names[i + 1] > "this") {
                names.splice(i + 1, 0, "this");
                break;
            }
        }

        return names;
    },

    getProperty: function (aObj, aName) {
        let result;
        // Try/catch since aName can be anything, and getVariable throws if
        // it's not a valid ECMAScript identifier name
        try {
            // TODO: we should use getVariableDescriptor() here - bug 725815.
            result = aObj.getVariable(aName);
        } catch (e) {
        }

        // FIXME: Need actual UI, bug 941287.
        if (result === undefined || result.optimizedOut || result.missingArguments) {
            return null;
        }
        return {value: result};
    }
};
