/*
 * Copyright (c) 2017 - 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.
 * 
 */

/*eslint-env node */
/*jslint node: true */
/*global setImmediate */



"use strict";

var config = {};
var _domainManager;
var MessageIds;
var ternOptions;
var self = {
    postMessage: function (data) {
        _domainManager.emitEvent("TernNodeDomain", "data", [data]);
    }
};

var Tern = require("tern"),
    Infer = require("tern/lib/infer");
    
require("tern/plugin/requirejs");
require("tern/plugin/doc_comment");
require("tern/plugin/angular");


var ExtractContent = require("./ExtractFileContent");

var ternServer  = null,
    isUntitledDoc = false,
    inferenceTimeout;

// Save the tern callbacks for when we get the contents of the file
var fileCallBacks = {};

/**
 * Send a log message back from the node to the main thread
 * @private
 * @param {string} msg - the log message
 */
function _log(msg) {
    console.log(msg);
}

/**
 * Report exception
 * @private
 * @param {Error} e - the error object
 */
function _reportError(e, file) {
    if (e instanceof Infer.TimedOut) {
        // Post a message back to the main thread with timedout info
        self.postMessage({
            type: MessageIds.TERN_INFERENCE_TIMEDOUT,
            file: file
        });
    } else {
        _log("Error thrown in tern_node domain:" + e.message + "\n" + e.stack);
    }
}

/**
 * Handle a response from the main thread providing the contents of a file
 * @param {string} file - the name of the file
 * @param {string} text - the contents of the file
 */
function handleGetFile(file, text) {
    var next = fileCallBacks[file];
    if (next) {
        try {
            next(null, text);
        } catch (e) {
            _reportError(e, file);
        }
    }
    delete fileCallBacks[file];
}

function _getNormalizedFilename(fileName) {
    if (!isUntitledDoc && ternServer.projectDir && fileName.indexOf(ternServer.projectDir) === -1) {
        fileName = ternServer.projectDir + fileName;
    }
    return fileName;
}

function _getDenormalizedFilename(fileName) {
    if (!isUntitledDoc && ternServer.projectDir && fileName.indexOf(ternServer.projectDir) === 0) {
        fileName = fileName.slice(ternServer.projectDir.length);
    }
    return fileName;
}

/**
 * Callback handle to request contents of a file from the main thread
 * @param {string} file - the name of the file
 */
function _requestFileContent(name) {
    self.postMessage({
        type: MessageIds.TERN_GET_FILE_MSG,
        file: name
    });
}

/**
 * Provide the contents of the requested file to tern
 * @param {string} name - the name of the file
 * @param {Function} next - the function to call with the text of the file
 *  once it has been read in.
 */
function getFile(name, next) {
    // save the callback
    fileCallBacks[name] = next;

    setImmediate(function () {
        try {
            ExtractContent.extractContent(name, handleGetFile, _requestFileContent);
        } catch (error) {
            console.log(error);
        }
    });
}

/**
 * Create a new tern server.
 *
 * @param {Object} env - an Object with the environment, as read in from
 *  the json files in thirdparty/tern/defs
 * @param {Array.<string>} files - a list of filenames tern should be aware of
 */
function initTernServer(env, files) {
    ternOptions = {
        defs: env,
        async: true,
        getFile: getFile,
        plugins: {requirejs: {}, doc_comment: true, angular: true},
        ecmaVersion: 9
    };

    // If a server is already created just reset the analysis data before marking it for GC
    if (ternServer) {
        ternServer.reset();
        Infer.resetGuessing();
    }
        
    ternServer = new Tern.Server(ternOptions);

    files.forEach(function (file) {
        ternServer.addFile(file);
    });

}

/**
 * Resets an existing tern server.
 */
function resetTernServer() {
    // If a server is already created just reset the analysis data 
    if (ternServer) {
        ternServer.reset();
        Infer.resetGuessing();
        // tell the main thread we're ready to start processing again
        self.postMessage({type: MessageIds.TERN_WORKER_READY});
    }
}

/**
 * Create a "empty" update object.
 *
 * @param {string} path - full path of the file.
 * @return {{type: string, name: string, offsetLines: number, text: string}} -
 * "empty" update.

 */
function createEmptyUpdate(path) {
    return {type: MessageIds.TERN_FILE_INFO_TYPE_EMPTY,
        name: path,
        offsetLines: 0,
        text: ""};
}

/**
 * Build an object that can be used as a request to tern.
 *
 * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo
 * - type of update, name of file, and the text of the update.
 * For "full" updates, the whole text of the file is present. For "part" updates,
 * the changed portion of the text. For "empty" updates, the file has not been modified
 * and the text is empty.
 * @param {string} query - the type of request being made
 * @param {{line: number, ch: number}} offset -
 */
function buildRequest(fileInfo, query, offset) {
    query = {type: query};
    query.start = offset;
    query.end = offset;
    query.file = (fileInfo.type === MessageIds.TERN_FILE_INFO_TYPE_PART) ? "#0" : fileInfo.name;
    query.filter = false;
    query.sort = false;
    query.depths = true;
    query.guess = true;
    query.origins = true;
    query.types = true;
    query.expandWordForward = false;
    query.lineCharPositions = true;
    query.docs = true;
    query.urls = true;

    var request = {query: query, files: [], offset: offset, timeout: inferenceTimeout};
    if (fileInfo.type !== MessageIds.TERN_FILE_INFO_TYPE_EMPTY) {
        // Create a copy to mutate ahead
        var fileInfoCopy = JSON.parse(JSON.stringify(fileInfo));
        request.files.push(fileInfoCopy);
    }

    return request;
}


/**
 * Get all References location
 * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo
 * - type of update, name of file, and the text of the update.
 * For "full" updates, the whole text of the file is present. For "part" updates,
 * the changed portion of the text. For "empty" updates, the file has not been modified
 * and the text is empty.
 * @param {{line: number, ch: number}} offset - the offset into the
 * file for cursor
 */
 function getRefs(fileInfo, offset) {
    var request = buildRequest(fileInfo, "refs", offset);
    try {
        ternServer.request(request, function (error, data) {
            if (error) {
                _log("Error returned from Tern 'refs' request: " + error);
                var response = {
                    type: MessageIds.TERN_REFS,
                    error: error.message
                };
                self.postMessage(response);
                return;
            }
            var response = {
                type: MessageIds.TERN_REFS,
                file: fileInfo.name,
                offset: offset,
                references: data
            };
            // Post a message back to the main thread with the results
            self.postMessage(response);
        });
    } catch (e) {
        _reportError(e, fileInfo.name);
    }
}

/**
 * Get scope at the offset in the file
 * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo
 * - type of update, name of file, and the text of the update.
 * For "full" updates, the whole text of the file is present. For "part" updates,
 * the changed portion of the text. For "empty" updates, the file has not been modified
 * and the text is empty.
 * @param {{line: number, ch: number}} offset - the offset into the
 * file for cursor
 */
function getScopeData(fileInfo, offset) {
    // Create a new tern Server
    // Existing tern server resolves all the required modules which might take time
    // We only need to analyze single file for getting the scope
    ternOptions.plugins = {};
    var ternServer = new Tern.Server(ternOptions);
    ternServer.addFile(fileInfo.name, fileInfo.text);

    var error;
    var request = buildRequest(fileInfo, "completions", offset); // for primepump

    try {
        // primepump
        ternServer.request(request, function (ternError, data) {
            if (ternError) {
                _log("Error for Tern request: \n" + JSON.stringify(request) + "\n" + ternError);
                error = ternError.toString();
            } else {
                var file = ternServer.findFile(fileInfo.name);
                var scope = Infer.scopeAt(file.ast, Tern.resolvePos(file, offset), file.scope);

                if (scope) {
                    // Remove unwanted properties to remove cycles in the object
                    scope = JSON.parse(JSON.stringify(scope, function(key, value) {
                        if (["proto", "propertyOf", "onNewProp", "sourceFile", "maybeProps"].includes(key)) {
                            return undefined;
                        }
                        else if (key === "fnType") {
                             return value.name || "FunctionExpression";
                        }
                        else if (key === "props") {
                            for (var key in value) {
                                value[key] = value[key].propertyName;
                            }
                            return value;
                        } else if (key === "originNode") {
                            return value && {
                                start: value.start,
                                end: value.end,
                                type: value.type,
                                body: {
                                    start: value.body.start,
                                    end: value.body.end
                                }
                            };
                        }

                        return value;
                    }));
                }

                self.postMessage({
                    type: MessageIds.TERN_SCOPEDATA_MSG,
                    file: _getNormalizedFilename(fileInfo.name),
                    offset: offset,
                    scope: scope
                });
            }
        });
    } catch (e) {
        _reportError(e, fileInfo.name);
    } finally {
        ternServer.reset();
        Infer.resetGuessing();
    }
}


/**
 * Get definition location
 * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo
 * - type of update, name of file, and the text of the update.
 * For "full" updates, the whole text of the file is present. For "part" updates,
 * the changed portion of the text. For "empty" updates, the file has not been modified
 * and the text is empty.
 * @param {{line: number, ch: number}} offset - the offset into the
 * file for cursor
 */
function getJumptoDef(fileInfo, offset) {
    var request = buildRequest(fileInfo, "definition", offset);
    // request.query.typeOnly = true;       // FIXME: tern doesn't work exactly right yet.

    try {
        ternServer.request(request, function (error, data) {
            if (error) {
                _log("Error returned from Tern 'definition' request: " + error);
                self.postMessage({type: MessageIds.TERN_JUMPTODEF_MSG, file: fileInfo.name, offset: offset});
                return;
            }
            var response = {
                type: MessageIds.TERN_JUMPTODEF_MSG,
                file: _getNormalizedFilename(fileInfo.name),
                resultFile: data.file,
                offset: offset,
                start: data.start,
                end: data.end
            };

            request = buildRequest(fileInfo, "type", offset);
            // See if we can tell if the reference is to a Function type
            ternServer.request(request, function (error, data) {
                if (!error) {
                    response.isFunction = data.type.length > 2 && data.type.substring(0, 2) === "fn";
                }

                // Post a message back to the main thread with the definition
                self.postMessage(response);
            });

        });
    } catch (e) {
        _reportError(e, fileInfo.name);
    }
}

/**
 * Get all the known properties for guessing.
 *
 * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo
 * - type of update, name of file, and the text of the update.
 * For "full" updates, the whole text of the file is present. For "part" updates,
 * the changed portion of the text. For "empty" updates, the file has not been modified
 * and the text is empty.
 * @param {{line: number, ch: number}} offset -
 * the offset into the file where we want completions for
 * @param {string} type     - the type of the message to reply with.
 */
function getTernProperties(fileInfo, offset, type) {

    var request = buildRequest(fileInfo, "properties", offset),
        i;
    //_log("tern properties: request " + request.type + dir + " " + file);
    try {
        ternServer.request(request, function (error, data) {
            var properties = [];
            if (error) {
                _log("Error returned from Tern 'properties' request: " + error);
            } else {
                //_log("tern properties: completions = " + data.completions.length);
                properties = data.completions.map(function (completion) {
                    return {value: completion, type: completion.type, guess: true};
                });
            }
            // Post a message back to the main thread with the completions
            self.postMessage({type: type,
                              file: _getNormalizedFilename(fileInfo.name),
                              offset: offset,
                              properties: properties
                });
        });
    } catch (e) {
        _reportError(e, fileInfo.name);
    }
}

/**
 * Get the completions for the given offset
 *
 * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo
 * - type of update, name of file, and the text of the update.
 * For "full" updates, the whole text of the file is present. For "part" updates,
 * the changed portion of the text. For "empty" updates, the file has not been modified
 * and the text is empty.
 * @param {{line: number, ch: number}} offset -
 * the offset into the file where we want completions for
 * @param {boolean} isProperty - true if getting a property hint,
 * otherwise getting an identifier hint.
 */
function getTernHints(fileInfo, offset, isProperty) {
    var request = buildRequest(fileInfo, "completions", offset),
        i;
    //_log("request " + dir + " " + file + " " + offset /*+ " " + text */);
    try {
        ternServer.request(request, function (error, data) {
            var completions = [];
            if (error) {
                _log("Error returned from Tern 'completions' request: " + error);
            } else {
                //_log("found " + data.completions + " for " + file + "@" + offset);
                completions = data.completions.map(function (completion) {
                    return {value: completion.name, type: completion.type, depth: completion.depth,
                        guess: completion.guess, origin: completion.origin, doc: completion.doc, url: completion.url};
                });
            }

            if (completions.length > 0 || !isProperty) {
                // Post a message back to the main thread with the completions
                self.postMessage({type: MessageIds.TERN_COMPLETIONS_MSG,
                    file: _getNormalizedFilename(fileInfo.name),
                    offset: offset,
                    completions: completions
                    });
            } else {
                // if there are no completions, then get all the properties
                getTernProperties(fileInfo, offset, MessageIds.TERN_COMPLETIONS_MSG);
            }
        });
    } catch (e) {
        _reportError(e, fileInfo.name);
    }
}

/**
 *  Given a Tern type object, convert it to an array of Objects, where each object describes
 *  a parameter.
 *
 * @param {!Infer.Fn} inferFnType - type to convert.
 * @return {Array<{name: string, type: string, isOptional: boolean}>} where each entry in the array is a parameter.
 */
function getParameters(inferFnType) {

    // work around define functions before use warning.
    var recordTypeToString, inferTypeToString, processInferFnTypeParameters, inferFnTypeToString;

    /**
     *  Convert an infer array type to a string.
     *
     *  Formatted using google closure style. For example:
     *
     *  "Array.<string, number>"
     *
     * @param {Infer.Arr} inferArrType
     *
     * @return {string} - array formatted in google closure style.
     *
     */
    function inferArrTypeToString(inferArrType) {
        var result = "Array.<";

        result += inferArrType.props["<i>"].types.map(inferTypeToString).join(", ");

        // workaround case where types is zero length
        if (inferArrType.props["<i>"].types.length === 0) {
            result += "Object";
        }
        result += ">";

        return result;
    }

    /**
     * Convert properties to a record type annotation.
     *
     * @param {Object} props
     * @return {string} - record type annotation
     */
    recordTypeToString = function (props) {
        var result = "{",
            first = true,
            prop;

        result += Object.keys(props).map(function (key) {
            return key + ": " + inferTypeToString(props[key]);
        }).join(", ");

        result += "}";

        return result;
    };

    /**
     *  Convert an infer type to a string.
     *
     * @param {*} inferType - one of the Infer's types; Infer.Prim, Infer.Arr, Infer.ANull. Infer.Fn functions are
     * not handled here.
     *
     * @return {string}
     *
     */
    inferTypeToString = function (inferType) {
        var result;

        if (inferType instanceof Infer.AVal) {
            inferType = inferType.types[0];
        }

        if (inferType instanceof Infer.Prim) {
            result = inferType.toString();
            if (result === "string") {
                result = "String";
            } else if (result === "number") {
                result = "Number";
            } else if (result === "boolean") {
                result = "Boolean";
            }
        } else if (inferType instanceof Infer.Arr) {
            result = inferArrTypeToString(inferType);
        } else if (inferType instanceof Infer.Fn) {
            result = inferFnTypeToString(inferType);
        } else if (inferType instanceof Infer.Obj) {
            if (inferType.name === undefined) {
                result = recordTypeToString(inferType.props);
            } else {
                result = inferType.name;
            }
        } else {
            result = "Object";
        }

        return result;
    };

    /**
     * Format the given parameter array. Handles separators between
     * parameters, syntax for optional parameters, and the order of the
     * parameter type and parameter name.
     *
     * @param {!Array.<{name: string, type: string, isOptional: boolean}>} params -
     * array of parameter descriptors
     * @param {function(string)=} appendSeparators - callback function to append separators.
     * The separator is passed to the callback.
     * @param {function(string, number)=} appendParameter - callback function to append parameter.
     * The formatted parameter type and name is passed to the callback along with the
     * current index of the parameter.
     * @param {boolean=} typesOnly - only show parameter types. The
     * default behavior is to include both parameter names and types.
     * @return {string} - formatted parameter hint
     */
    function formatParameterHint(params, appendSeparators, appendParameter, typesOnly) {
        var result = "",
            pendingOptional = false;

        params.forEach(function (value, i) {
            var param = value.type,
                separators = "";

            if (value.isOptional) {
                // if an optional param is following by an optional parameter, then
                // terminate the bracket. Otherwise enclose a required parameter
                // in the same bracket.
                if (pendingOptional) {
                    separators += "]";
                }

                pendingOptional = true;
            }

            if (i > 0) {
                separators += ", ";
            }

            if (value.isOptional) {
                separators += "[";
            }

            if (appendSeparators) {
                appendSeparators(separators);
            }

            result += separators;

            if (!typesOnly) {
                param += " " + value.name;
            }

            if (appendParameter) {
                appendParameter(param, i);
            }

            result += param;

        });

        if (pendingOptional) {
            if (appendSeparators) {
                appendSeparators("]");
            }

            result += "]";
        }

        return result;
    }

    /**
     * Convert an infer function type to a Google closure type string.
     *
     * @param {Infer.Fn} inferType - type to convert.
     * @return {string} - function type as a string.
     */
    inferFnTypeToString = function (inferType) {
        var result = "function(",
            params = processInferFnTypeParameters(inferType);

        result += /*HintUtils2.*/formatParameterHint(params, null, null, true);
        if (inferType.retval) {
            result += "):";
            result += inferTypeToString(inferType.retval);
        }

        return result;
    };

    /**
     * Convert an infer function type to string.
     *
     * @param {*} inferType - one of the Infer's types; Infer.Fn, Infer.Prim, Infer.Arr, Infer.ANull
     * @return {Array<{name: string, type: string, isOptional: boolean}>} where each entry in the array is a parameter.
     */
    processInferFnTypeParameters = function (inferType) {
        var params = [],
            i;

        for (i = 0; i < inferType.args.length; i++) {
            var param = {},
                name = inferType.argNames[i],
                type = inferType.args[i];

            if (!name) {
                name = "param" + (i + 1);
            }

            if (name[name.length - 1] === "?") {
                name = name.substring(0, name.length - 1);
                param.isOptional = true;
            }

            param.name = name;
            param.type = inferTypeToString(type);
            params.push(param);
        }

        return params;
    };

    return processInferFnTypeParameters(inferFnType);
}

/**
 * Get the function type for the given offset
 *
 * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo
 * - type of update, name of file, and the text of the update.
 * For "full" updates, the whole text of the file is present. For "part" updates,
 * the changed portion of the text. For "empty" updates, the file has not been modified
 * and the text is empty.
 * @param {{line: number, ch: number}} offset -
 * the offset into the file where we want completions for
 */
function handleFunctionType(fileInfo, offset) {
    var request = buildRequest(fileInfo, "type", offset),
        error;

    request.query.preferFunction = true;

    var fnType = "";
    try {
        ternServer.request(request, function (ternError, data) {

            if (ternError) {
                _log("Error for Tern request: \n" + JSON.stringify(request) + "\n" + ternError);
                error = ternError.toString();
            } else {
                var file = ternServer.findFile(fileInfo.name);

                // convert query from partial to full offsets
                var newOffset = offset;
                if (fileInfo.type === MessageIds.TERN_FILE_INFO_TYPE_PART) {
                    newOffset = {line: offset.line + fileInfo.offsetLines, ch: offset.ch};
                }

                request = buildRequest(createEmptyUpdate(fileInfo.name), "type", newOffset);

                var expr = Tern.findQueryExpr(file, request.query);
                Infer.resetGuessing();
                var type = Infer.expressionType(expr);
                type = type.getFunctionType() || type.getType();

                if (type) {
                    fnType = getParameters(type);
                } else {
                    ternError = "No parameter type found";
                    _log(ternError);
                }
            }
        });
    } catch (e) {
        _reportError(e, fileInfo.name);
    }

    // Post a message back to the main thread with the completions
    self.postMessage({type: MessageIds.TERN_CALLED_FUNC_TYPE_MSG,
        file: _getNormalizedFilename(fileInfo.name),
        offset: offset,
        fnType: fnType,
        error: error
        });
}

/**
 *  Add an array of files to tern.
 *
 * @param {Array.<string>} files - each string in the array is the full
 * path of a file.
 */
function handleAddFiles(files) {
    files.forEach(function (file) {
        ternServer.addFile(file);
    });
}

/**
 *  Update the context of a file in tern.
 *
 * @param {string} path - full path of file.
 * @param {string} text - content of the file.
 */
function handleUpdateFile(path, text) {

    ternServer.addFile(path, text);

    self.postMessage({type: MessageIds.TERN_UPDATE_FILE_MSG,
        path: path
        });

    // reset to get the best hints with the updated file.
    ternServer.reset();
    Infer.resetGuessing();
}

/**
 *  Make a completions request to tern to force tern to resolve files
 *  and create a fast first lookup for the user.
 * @param {string} path     - the path of the file
 */
function handlePrimePump(path) {
    var fileName = _getDenormalizedFilename(path);
    var fileInfo = createEmptyUpdate(fileName),
        request = buildRequest(fileInfo, "completions", {line: 0, ch: 0});

    try {
        ternServer.request(request, function (error, data) {
            // Post a message back to the main thread
            self.postMessage({type: MessageIds.TERN_PRIME_PUMP_MSG,
                path: _getNormalizedFilename(path)
                });
        });
    } catch (e) {
        _reportError(e, path);
    }
}

/**
 * Updates the configuration, typically for debugging purposes.
 *
 * @param {Object} configUpdate new configuration
 */
function setConfig(configUpdate) {
    config = configUpdate;
}

function _requestTernServer(commandConfig) {
    var file, text, offset,
        request = commandConfig,
        type = request.type;
    if (config.debug) {
        _log("Message received " + type);
    }

    if (type === MessageIds.TERN_INIT_MSG) {
        var env     = request.env,
            files   = request.files;
        inferenceTimeout = request.timeout;
        initTernServer(env, files);
    } else if (type === MessageIds.TERN_COMPLETIONS_MSG) {
        offset  = request.offset;
        getTernHints(request.fileInfo, offset, request.isProperty);
    } else if (type === MessageIds.TERN_GET_FILE_MSG) {
        file = request.file;
        text = request.text;
        handleGetFile(file, text);
    } else if (type === MessageIds.TERN_CALLED_FUNC_TYPE_MSG) {
        offset  = request.offset;
        handleFunctionType(request.fileInfo, offset);
    } else if (type === MessageIds.TERN_JUMPTODEF_MSG) {
        offset  = request.offset;
        getJumptoDef(request.fileInfo, offset);
    } else if (type === MessageIds.TERN_SCOPEDATA_MSG) {
        offset  = request.offset;
        getScopeData(request.fileInfo, offset);
    } else if (type === MessageIds.TERN_REFS) {
        offset  = request.offset;
        getRefs(request.fileInfo, offset);
    } else if (type === MessageIds.TERN_ADD_FILES_MSG) {
        handleAddFiles(request.files);
    } else if (type === MessageIds.TERN_PRIME_PUMP_MSG) {
        isUntitledDoc = request.isUntitledDoc;
        handlePrimePump(request.path);
    } else if (type === MessageIds.TERN_GET_GUESSES_MSG) {
        offset  = request.offset;
        getTernProperties(request.fileInfo, offset, MessageIds.TERN_GET_GUESSES_MSG);
    } else if (type === MessageIds.TERN_UPDATE_FILE_MSG) {
        handleUpdateFile(request.path, request.text);
    } else if (type === MessageIds.SET_CONFIG) {
        setConfig(request.config);
    } else if (type === MessageIds.TERN_UPDATE_DIRTY_FILE) {
        ExtractContent.updateFilesCache(request.name, request.action);
    } else if (type === MessageIds.TERN_CLEAR_DIRTY_FILES_LIST) {
        ExtractContent.clearFilesCache();
    } else {
        _log("Unknown message: " + JSON.stringify(request));
    }
}

function invokeTernCommand(commandConfig) {
    try {
        _requestTernServer(commandConfig);
    } catch (error) {
        console.warn(error);
    }
}

function setInterface(msgInterface) {
    MessageIds = msgInterface.messageIds;
}

function checkInterfaceAndReInit() {
    if (!MessageIds) {
        // WTF - Worse than failure
        // We are here as node process got restarted 
        // Request for ReInitialization of interface and Tern Server
        self.postMessage({
            type: "RE_INIT_TERN"
        });
    }
}

 /**
 * Initialize the test domain with commands and events related to find in files.
 * @param {DomainManager} domainManager The DomainManager for the TernNodeDomain
 */
function init(domainManager) {
    if (!domainManager.hasDomain("TernNodeDomain")) {
        domainManager.registerDomain("TernNodeDomain", {major: 0, minor: 1});
    }

    _domainManager = domainManager;

    domainManager.registerCommand(
        "TernNodeDomain",       // domain name
        "invokeTernCommand",    // command name
        invokeTernCommand,   // command handler function
        false,          // this command is synchronous in Node
        "Invokes a tern command on node",
        [{name: "commandConfig", // parameters
            type: "object",
            description: "Object containing tern command configuration"}]
    );

    domainManager.registerCommand(
        "TernNodeDomain",       // domain name
        "setInterface",    // command name
        setInterface,   // command handler function
        false,          // this command is synchronous in Node
        "Sets the shared message interface",
        [{name: "msgInterface", // parameters
            type: "object",
            description: "Object containing messageId enums"}]
    );

    domainManager.registerCommand(
        "TernNodeDomain",       // domain name
        "resetTernServer",    // command name
        resetTernServer,   // command handler function
        true,          // this command is synchronous in Node
        "Resets an existing tern server"
    );

    domainManager.registerEvent(
        "TernNodeDomain",     // domain name
        "data",   // event name
        [
            {
                name: "data",
                type: "Object",
                description: "data to be returned to main thread"
            }
        ]
    );
    setTimeout(checkInterfaceAndReInit, 1000);
}

exports.init = init;
