/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.javascript2.model.api;

import com.oracle.js.parser.ir.Node;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.javascript2.doc.spi.JsDocumentationHolder;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
import org.netbeans.modules.javascript2.types.api.DeclarationScope;
import org.netbeans.modules.javascript2.types.api.Identifier;
import org.netbeans.modules.javascript2.types.api.Type;
import org.netbeans.modules.javascript2.types.api.TypeUsage;
import org.netbeans.modules.javascript2.model.DeclarationScopeImpl;
import org.netbeans.modules.javascript2.model.EmbeddingHelper;
import org.netbeans.modules.javascript2.model.JsFunctionImpl;
import org.netbeans.modules.javascript2.model.JsFunctionReference;
import org.netbeans.modules.javascript2.model.JsObjectImpl;
import org.netbeans.modules.javascript2.model.JsObjectReference;
import org.netbeans.modules.javascript2.model.ModelBuilder;
import org.netbeans.modules.javascript2.model.ModelExtender;
import org.netbeans.modules.javascript2.model.ParameterObject;
import org.netbeans.modules.javascript2.model.SemiTypeResolverVisitor;
import org.netbeans.modules.javascript2.model.spi.TypeNameConvertor;
import org.netbeans.modules.javascript2.types.spi.ParserResult;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.spi.indexing.support.IndexResult;
import org.openide.filesystems.FileObject;

/**
 *
 * @author Petr Pisl
 */
public class ModelUtils {
      
    public static final String PROTOTYPE = "prototype"; //NOI18N
    
    public static final String CONSTRUCTOR = "constructor"; //NOI18N
    
    public static final String THIS = "this"; //NOI18N

    public static final String ARGUMENTS = "arguments"; //NOI18N

    private static final String GENERATED_FUNCTION_PREFIX = "L#"; //NOI18N
    
    private static final String GENERATED_ANONYM_PREFIX = "Anonym#"; //NOI18N
    
    private static final List<String> KNOWN_TYPES = Arrays.asList(Type.ARRAY, Type.STRING, Type.BOOLEAN, Type.NUMBER, Type.UNDEFINED);
    
    private static final int MAX_RECURSION_DEEP_RESOLVING_ASSIGNMENTS = 10;
    
    private static String GLOBAL_DIRECTIVE = "global"; //NOI18N
    
    private static final Logger LOG = Logger.getLogger(ModelUtils.class.getName());
    
    public static JsObjectImpl getJsObject (ModelBuilder builder, List<Identifier> fqName, boolean isLHS) {
        if (fqName == null || fqName.isEmpty()) {
            return null;
        }
        JsObject result = builder.getCurrentObject();
        JsObject tmpObject = null;
        String firstName = fqName.get(0).getName();
        
        while (tmpObject == null && result != null && result.getParent() != null) {
            if (result instanceof JsFunctionImpl) {
                tmpObject = ((JsFunctionImpl)result).getParameter(firstName);
            }
            if (tmpObject == null) {
                if (result.getProperty(firstName) != null) {
                    tmpObject = result;
                }
                result = result.getParent();
            } else {
                result = tmpObject;
            }
        }
        if (tmpObject == null) {
            JsObject current = builder.getCurrentObject();
            if (current instanceof JsWith) {
                tmpObject = current;
            } else {
                DeclarationScope scope = builder.getCurrentDeclarationFunction();
                while (scope != null && tmpObject == null && scope.getParentScope() != null) {
                    if (scope instanceof JsFunction) {
                        tmpObject = ((JsFunction)scope).getParameter(firstName);
                    }
                    if (tmpObject == null) {
                        tmpObject = ((JsObject)scope).getProperty(firstName);
                    }
                    scope = scope.getParentScope();
                }
                if (tmpObject == null) {
                    tmpObject = builder.getGlobal();
                } else {
                    result = tmpObject;
                }
            }
        }
        for (int index = (tmpObject instanceof ParameterObject ? 1 : 0); index < fqName.size() ; index++) {
            Identifier name = fqName.get(index);
            if (name != null) {
                result = tmpObject.getProperty(name.getName());
                if (result == null) {
                    result = new JsObjectImpl(tmpObject, name, name.getOffsetRange(),
                            (index < (fqName.size() - 1)) ? false : isLHS, tmpObject.getMimeType(), tmpObject.getSourceLabel());
                    tmpObject.addProperty(name.getName(), result);
                }
            }
            tmpObject = result;
        }
        return (JsObjectImpl)result;        
    }

    public static boolean isGlobal(JsObject object) {
        return object != null && object.getJSKind() == JsElement.Kind.FILE;
    }

    public static boolean isDescendant(JsObject possibleDescendant, JsObject possibleAncestor) {
        JsObject parent = possibleDescendant;
        while (parent != null && !parent.equals(possibleAncestor)) {
            parent = parent.getParent();
        }
        return parent != null;
    }
    
    public static JsObject findJsObject(Model model, int offset) {
        JsObject result = null;
        JsObject global = model.getGlobalObject();
        result = findJsObject(global, offset);
        if (result == null) {
            result = global;
        }
        return result;
    }
    
    public static JsObject findJsObject(JsObject object, int offset) {
        HashSet<String> visited = new HashSet<String>();
        return findJsObject(object, offset, visited);
    }

    public static void copyOccurrences(JsObject from, JsObject to) {
        for (Occurrence oc : from.getOccurrences()) {
            to.addOccurrence(oc.getOffsetRange());
        }
    }
    
    public static JsObject findJsObject(JsObject object, int offset, Set<String> visited) {
        JsObjectImpl jsObject = (JsObjectImpl)object;
        visited.add(jsObject.getFullyQualifiedName());
        JsObject result = null;
        JsObject tmpObject = null;
        if (jsObject.containsOffset(offset)) {
            result = jsObject;
            for (JsObject property : jsObject.getProperties().values()) {
                JsElement.Kind kind = property.getJSKind();
                if (kind == JsElement.Kind.OBJECT || kind == JsElement.Kind.ANONYMOUS_OBJECT || kind == JsElement.Kind.OBJECT_LITERAL
                        || kind == JsElement.Kind.FUNCTION || kind == JsElement.Kind.METHOD || kind == JsElement.Kind.CONSTRUCTOR
                        || kind == JsElement.Kind.WITH_OBJECT) {
                    if (!visited.contains(property.getFullyQualifiedName())) {
                        tmpObject = findJsObject(property, offset, visited);
                    }
                }
                if (tmpObject != null) {
                    result = tmpObject;
                    break;
                }
            }
            if ( object.getJSKind() == JsElement.Kind.WITH_OBJECT) {
                for (JsWith innerWith :((JsWith)object).getInnerWiths()){
                    if (!visited.contains(innerWith.getFullyQualifiedName())) {
                        tmpObject = findJsObject(innerWith, offset, visited);
                    }
                    if (tmpObject != null) {
                        result = tmpObject;
                        break;
                    }
                }
            }
            if (object instanceof JsArray) {
                JsArray array = (JsArray)object;
                for (TypeUsage type : array.getTypesInArray()) {
                    if (type.getType().startsWith(SemiTypeResolverVisitor.ST_ANONYM)) {
                        int anonymOffset = Integer.parseInt(type.getType().substring(SemiTypeResolverVisitor.ST_ANONYM.length()));
                        if (anonymOffset > 0) {
                            DeclarationScope scope = getDeclarationScope(array);
                            for (JsObject property : ((JsObject)scope).getProperties().values()) {
                                JsElement.Kind kind = property.getJSKind();
                                if (kind == JsElement.Kind.ANONYMOUS_OBJECT && !visited.contains(property.getFullyQualifiedName())) { 
                                    tmpObject = findJsObject(property, offset, visited);
                                }
                                if (tmpObject != null) {
                                    result = tmpObject;
                                    break;
                                }
                            }
                        }   
                    }
                }
            }
        }
        return result;
    }
    
    public static JsObject findJsObjectByName(JsObject global, String fqName) {
        JsObject result = global;
        JsObject property = result;
        for (StringTokenizer stringTokenizer = new StringTokenizer(fqName, "."); stringTokenizer.hasMoreTokens() && result != null;) {
            String token = stringTokenizer.nextToken();
            property = result.getProperty(token);
            if (property == null) {
                result = (result instanceof JsFunction)
                        ? ((JsFunction)result).getParameter(token)
                        : null;
                if (result == null) {
                    break;
                }
            } else {
                result = property;
            }
        }
        return result;
    }
    
    public static JsObject findJsObjectByName(Model model, String fqName) {
        return findJsObjectByName(model.getGlobalObject(), fqName);
    }
    
    public static JsObject getGlobalObject(JsObject jsObject) {
        JsObject result = jsObject;
        while(result.getParent() != null) {
            result = result.getParent();
        }
        return result;
    }

    public static DeclarationScope getDeclarationScope(JsObject object) {
        assert object != null;

        JsObject result =  object;
        while (result.getParent() != null && !(result.getParent() instanceof DeclarationScope)) {
            result = result.getParent();
        }
        if (result.getParent() != null && result.getParent() instanceof DeclarationScope) {
            result = result.getParent();
        } 
        if (!(result instanceof DeclarationScope)) {
            // this shouldn't happened, basically it means that the model is broken and has an object without parent
            result = getGlobalObject(object);
        }
        return (DeclarationScope)result;
    }

    public static DeclarationScope getDeclarationScope(Model model, int offset) {
        DeclarationScope result = null;
        JsObject global = model.getGlobalObject();
        result = getDeclarationScope((DeclarationScope)global, offset);
        if (result == null) {
            result = (DeclarationScope)global;
        }
        return result;
    }
    
    public static DeclarationScope getDeclarationScope(DeclarationScope scope, int offset) {
        
        DeclarationScopeImpl dScope = (DeclarationScopeImpl)scope;
        DeclarationScope result = null; 
        if (result == null) {
            if (dScope.getOffsetRange().containsInclusive(offset)) {
                result = dScope;
                boolean deep = true;
                while (deep) {
                    deep = false;
                    for (DeclarationScope innerScope : result.getChildrenScopes()) {
                        if ((innerScope instanceof DeclarationScopeImpl)
                                && ((DeclarationScopeImpl)innerScope).getOffsetRange().containsInclusive(offset)) {
                            result = innerScope;
                            deep = true;
                            break;
                        }
                        
                    }
                }
            }
        }
        return result;
    }
    
    public static OffsetRange documentOffsetRange(ParserResult result, int start, int end) {
        int lStart = LexUtilities.getLexerOffset(result, start);
        int lEnd = LexUtilities.getLexerOffset(result, end);
        if (lStart == -1 || lEnd == -1) {
            return OffsetRange.NONE;
        }
        if (lEnd < lStart) {
            // TODO this is a workaround for bug in nashorn, when sometime the start and end are not crorrect
            int length = lStart - lEnd;
            lEnd = lStart + length;
        }
        return new OffsetRange(lStart, lEnd);
    }
    
    /**
     * Returns all variables that are available in the scope
     * @param inScope
     * @return 
     */
    public static Collection<? extends JsObject> getVariables(DeclarationScope inScope) {
        HashMap<String, JsObject> result = new HashMap<String, JsObject>();
        while (inScope != null) {
            for (JsObject object : ((JsObject)inScope).getProperties().values()) {
                if (!result.containsKey(object.getName()) && object.getModifiers().contains(Modifier.PRIVATE)) {
                    result.put(object.getName(), object);
                }
            }
            if (inScope instanceof JsFunction) {
                for (JsObject object : ((JsFunction)inScope).getParameters()) {
                    if (!result.containsKey(object.getName())) {
                        result.put(object.getName(), object);
                    }
                }
            }
            for (JsObject object : ((JsObject)inScope).getProperties().values()) {
                if (!result.containsKey(object.getName())) {
                    result.put(object.getName(), object);
                }
            }
            if (inScope.getParentScope() != null && !result.containsKey(((JsObject)inScope).getName())) {
                result.put(((JsObject)inScope).getName(), (JsObject)inScope);
            }
            inScope = inScope.getParentScope();
        }
        return result.values();
    }
    
    public static JsObject getScopeVariable(DeclarationScope inScope, String name) {
        for( DeclarationScope curScope = inScope; curScope != null; curScope = curScope.getParentScope()) {
            JsObject prop = ((JsObject)curScope).getProperty(name);
            
            if( prop != null && prop.getModifiers().contains(Modifier.PRIVATE) )
                return prop;
            
            if( curScope instanceof JsFunction ) {
                JsObject param = ((JsFunction)curScope).getParameter(name);
                if( param != null )
                    return param;
            }
            
            if( prop != null )
                return prop;
            
            if( name.equals(((JsObject)inScope).getName()) )
                return (JsObject)inScope;
        }
        
        return null;
    }

    public static Collection<? extends JsObject> getVariables(Model model, int offset) {
        DeclarationScope scope = ModelUtils.getDeclarationScope(model, offset);
        return  getVariables(scope);
    }
    
    public static JsObject getJsObjectByName(DeclarationScope inScope, String simpleName) {
        Collection<? extends JsObject> variables = ModelUtils.getVariables(inScope);
        for (JsObject jsObject : variables) {
            if (simpleName.equals(jsObject.getName())) {
                return jsObject;
            }
        }
        return null;
    }
    private static final Collection<JsTokenId> CTX_DELIMITERS = Arrays.asList(
            JsTokenId.BRACKET_LEFT_CURLY, JsTokenId.BRACKET_RIGHT_CURLY,
            JsTokenId.OPERATOR_SEMICOLON);

    private static Collection<TypeUsage> tryResolveWindowProperty(Model model, Index jsIndex, String name) {
        // since issue #215863
        String fqn = null;
        int offset = -1;
        for (IndexedElement indexedElement : jsIndex.getProperties("window")) { //NOI18N
            if (indexedElement.getName().equals(name)) {
                offset = indexedElement.getOffset();
                fqn = "window." + name;
                break;
            }
        }
        if (fqn == null) {
            for (IndexedElement indexedElement : jsIndex.getProperties("Window.prototype")) { //NOI18N
                if (indexedElement.getName().equals(name)) {
                    offset = indexedElement.getOffset();
                    fqn = "Window.prototype." + name; //NOI18N
                    break;
                }
            }
        }
        if (fqn != null) {
            List<TypeUsage> fromAssignment = new ArrayList<TypeUsage>();
            resolveAssignments(model, jsIndex, fqn, offset, fromAssignment);
            if (fromAssignment.isEmpty()) {
                fromAssignment.add(new TypeUsage(fqn));
            } 
            return fromAssignment;
        }
        return null;
    }
    
    private enum State {
        INIT
    }

    public static Collection<TypeUsage> resolveSemiTypeOfExpression(ModelBuilder builder, Node expression) {
        Collection<TypeUsage> result = new HashSet<TypeUsage>();
        SemiTypeResolverVisitor visitor = new SemiTypeResolverVisitor();
        if (expression != null) {
            result = visitor.getSemiTypes(expression, builder);
        }
        if (builder.getCurrentWith()!= null) {
            Collection<TypeUsage> withResult = new HashSet<TypeUsage>();
            String withSemi = SemiTypeResolverVisitor.ST_WITH + builder.getCurrentWith().getFullyQualifiedName();
            
            for(TypeUsage type : result) {
                if (!KNOWN_TYPES.contains(type.getType())) {
                    withResult.add(new TypeUsage(withSemi + type.getType(), type.getOffset(), type.isResolved()));
                } else {
                    withResult.add(type);
                }
            }
            result = withResult;
        }
        return result;
    }
    
    public static Collection<TypeUsage> resolveTypeFromSemiType(JsObject object, TypeUsage type) {
        Set<TypeUsage> result = new HashSet<TypeUsage>();
        if (type.isResolved()) {
            result.add(type);
        } else if (Type.UNDEFINED.equals(type.getType())) {
            if (object.getJSKind() == JsElement.Kind.CONSTRUCTOR) {
                if (object.getParent().getJSKind() == JsElement.Kind.CLASS) {
                    result.add(new TypeUsage(object.getParent().getFullyQualifiedName(), type.getOffset(), true));
                } else {
                    result.add(new TypeUsage(object.getFullyQualifiedName(), type.getOffset(), true));
                }
            } else {
                result.add(new TypeUsage(Type.UNDEFINED, type.getOffset(), true));
            }
        } else if (EmbeddingHelper.containsGeneratedIdentifier(type.getType())) {
            result.add(new TypeUsage(Type.UNDEFINED, type.getOffset(), true));
        } else if (SemiTypeResolverVisitor.ST_THIS.equals(type.getType())) {
            JsObject parent = resolveThis(object);
            if (parent != null) {
                result.add(new TypeUsage(parent.getFullyQualifiedName(), type.getOffset(), true));
            }
        } else if (type.getType().startsWith(SemiTypeResolverVisitor.ST_THIS)) {
             JsObject parent = resolveThis(object);
            if (parent != null) {
                Collection<TypeUsage> locally = resolveSemiTypeChain(parent, type.getType().substring(6));
                if (locally.isEmpty()) {
                    result.add(new TypeUsage(type.getType().replace(SemiTypeResolverVisitor.ST_THIS, parent.getFullyQualifiedName()), type.getOffset(), false));
                } else {
                    if (locally.size() == 1) {
                        TypeUsage localType = locally.iterator().next();
                        if (localType.isResolved()) {
                            JsObject rObject = ModelUtils.findJsObjectByName(ModelUtils.getGlobalObject(object), localType.getType());
                            JsFunction function = rObject instanceof JsFunctionImpl
                                    ? (JsFunctionImpl) rObject
                                    : rObject instanceof JsFunctionReference ? ((JsFunctionReference) rObject).getOriginal() : null;
                            if (function != null && function.getParent() != null && object != null
                                    && function.getParent().equals(object.getParent())
                                    && object.getDeclarationName() != null) {
                                // creates reference to the original function
                                object.getParent().addProperty(object.getName(), new JsFunctionReference(
                                        object.getParent(), object.getDeclarationName(), function, true, null));
                            } 
                        } 
                    }
                    result.addAll(locally);
                }
            }
        } else if (type.getType().startsWith(SemiTypeResolverVisitor.ST_NEW)) {
            result.addAll(resolveSemiTypeCallChain(object, type));
        } else if (type.getType().startsWith(SemiTypeResolverVisitor.ST_CALL)) {
            result.addAll(resolveSemiTypeCallChain(object, type));
        } else if(type.getType().startsWith(SemiTypeResolverVisitor.ST_ANONYM)){
            String offsetPart = type.getType().substring(8);
            String rest = "";
            int index = offsetPart.indexOf(SemiTypeResolverVisitor.ST_START_DELIMITER);
            if (index > -1) {
                rest = offsetPart.substring(index);
                offsetPart = offsetPart.substring(0, index);
            }
            int start = Integer.parseInt(offsetPart);
//            JsObject globalObject = ModelUtils.getGlobalObject(object);
            JsObject byOffset = ModelUtils.findJsObject(object, start);
            if (byOffset == null) {
                JsObject globalObject = ModelUtils.getGlobalObject(object);
                byOffset = ModelUtils.findJsObject(globalObject, start);
            }
            if(byOffset != null && byOffset.isAnonymous()) {
                if (rest.isEmpty()) {
                    result.add(new TypeUsage(byOffset.getFullyQualifiedName(), byOffset.getOffset(), true));
                } else {
                    String newType= SemiTypeResolverVisitor.ST_EXP + byOffset.getFullyQualifiedName().replace(".", SemiTypeResolverVisitor.ST_PRO);
                    newType = newType + rest;
                    result.add(new TypeUsage(newType, byOffset.getOffset(), false));
                }
            }
//            for(JsObject children : globalObject.getProperties().values()) {
//                if(children.getOffset() == start && children.getName().startsWith("Anonym$")) {
//                    result.add(new TypeUsage(ModelUtils.createFQN(children), children.getOffset(), true));
//                    break;
//                }
//                
//            }
        } else if(type.getType().startsWith(SemiTypeResolverVisitor.ST_VAR)){
            String name = type.getType().substring(5);
            DeclarationScope declarationScope = object instanceof DeclarationScope ? (DeclarationScope)object : getDeclarationScope(object);
            List<JsObject> variables = new ArrayList(ModelUtils.getVariables(declarationScope));
            if (!(object instanceof DeclarationScope) && object.getParent() != null && !(object.getParent() instanceof DeclarationScope)) {
                variables.addAll(object.getParent().getProperties().values());
            }
            if (declarationScope != null) {
                boolean resolved = false;
                for (JsObject variable : variables) {
                    if (variable.getName().equals(name)) {
                        String newVarType;
                        if (!variable.getAssignments().isEmpty()) {
                             newVarType= SemiTypeResolverVisitor.ST_EXP + variable.getFullyQualifiedName().replace(".", SemiTypeResolverVisitor.ST_PRO);
                             result.add(new TypeUsage(newVarType, type.getOffset(), false));
                             resolved = true;
                             break;
                        } else {
                            if (variable.getJSKind() != JsElement.Kind.PARAMETER) {
                                if (variable.getJSKind().isFunction() && object.getAssignments().size() == 1
                                        && object.getParent() != null && object.getDeclarationName() != null) {
                                    JsObject oldProperty = object.getParent().getProperty(object.getName());
                                    JsObject newProperty = new JsFunctionReference(object.getParent(), object.getDeclarationName(), (JsFunction)variable, true, oldProperty.getModifiers());
                                    for (Occurrence occurrence : oldProperty.getOccurrences()) {
                                        newProperty.addOccurrence(occurrence.getOffsetRange());
                                    }
                                    object.getParent().addProperty(object.getName(), newProperty);
                                    
                                } else {
                                    newVarType = variable.getFullyQualifiedName();
                                    result.add(new TypeUsage(newVarType, type.getOffset(), false));
                                }
                                resolved = true;
                                break;
                            }
                        }
                    }
                }
                if (!resolved) {
                    if (declarationScope instanceof JsFunction) {
                        Collection<? extends JsObject> parameters = ((JsFunction)declarationScope).getParameters();
                        boolean isParameter = false;
                        for (JsObject parameter : parameters) {
                            if (name.equals(parameter.getName())) {
                                Collection<? extends TypeUsage> assignments = parameter.getAssignmentForOffset(parameter.getOffset());
                                result.addAll(assignments);
                                isParameter = true;
                                break;
                            }
                        }
                        if (!isParameter) {
                            result.add(new TypeUsage(name, type.getOffset(), false));
                        }
                    }
                }
            }
        } else if(type.getType().startsWith("@param;")) {
            String functionName = type.getType().substring(7);
            int index = functionName.indexOf(":");
            if (index > 0) {
                String fqn = functionName.substring(0, index);
                JsObject globalObject = ModelUtils.getGlobalObject(object);
                JsObject function = ModelUtils.findJsObjectByName(globalObject, fqn);
                if(function instanceof JsFunction) {
                    JsObject param = ((JsFunction)function).getParameter(functionName.substring(index + 1));
                    if(param != null) {
                        result.addAll(param.getAssignments());
                    }
                }
            }

        } else {
            result.add(type);
        }
        return result;
    }
    
    private static JsObject resolveThis(JsObject object) {
        JsObject parent = null;
        if (object.getJSKind() == JsElement.Kind.CONSTRUCTOR) {
            parent = object;
        } else {
            if (object.getParent() != null && object.getParent().getJSKind() != JsElement.Kind.FILE) {
                parent = object.getParent();
            } else {
                parent = object;
            }
        }
        if (parent != null && (parent.getJSKind() == JsElement.Kind.FUNCTION || parent.getJSKind() == JsElement.Kind.METHOD)) {
            if (parent.getParent().getJSKind() != JsElement.Kind.FILE) {
                JsObject grandParent = parent.getParent();
                if (grandParent != null 
                        && (grandParent.getJSKind() == JsElement.Kind.OBJECT_LITERAL || PROTOTYPE.equals(grandParent.getName()))) {
                    parent = grandParent;
                    if (PROTOTYPE.equals(parent.getName()) && parent.getParent() != null) {
                        parent = parent.getParent();
                    }
                }
            }
        }
        // if the parent is priviliged the this refers the constructor => find the constructor
        while (parent != null && parent.getParent() != null && parent.getModifiers().contains(Modifier.PROTECTED)) {
            parent = parent.getParent();
        }
        return parent;
    }
    
    private static Collection<TypeUsage> resolveSemiTypeCallChain(JsObject object, TypeUsage type) {
        Set<TypeUsage> result = new HashSet<TypeUsage>();
        DeclarationScope declarationScope = ModelUtils.getDeclarationScope(object);
        JsObject function = null;
        boolean calledNew = false;
        int index = -1;
        int dotIndex = -1;
        if (type.getType().startsWith(SemiTypeResolverVisitor.ST_CALL)) {
            index = 6;
        } else if (type.getType().startsWith(SemiTypeResolverVisitor.ST_NEW)) {
            index = 5;
            calledNew = true;
        }
        String name = type.getType().substring(index);
        if (declarationScope != null) {
            index = name.indexOf(SemiTypeResolverVisitor.ST_START_DELIMITER);
            if (index > -1) {
                name = name.substring(0, index);
            }
            Collection<? extends JsObject> variables = ModelUtils.getVariables(declarationScope);
            dotIndex = name.indexOf('.');
            String firstSpace = dotIndex == -1 ? name : name.substring(0, name.indexOf('.'));
            
            for (JsObject variable : variables) {
                if (variable.getName().equals(firstSpace)) {
                    function = variable;
                    break;
                }
            }
        }
        if (dotIndex != -1 && function != null) {
            function = ModelUtils.findJsObjectByName(function, name.substring(dotIndex + 1));
        }
        if (function != null) {
            if (index == -1) {
                if (function instanceof JsFunction) {
                    if (calledNew) {
                        result.add(new TypeUsage(function.getFullyQualifiedName(), type.getOffset(), true));
                    } else {
                        result.addAll(((JsFunction) function).getReturnTypes());
                    }
                } else {
                    if (calledNew) {
                        result.add(new TypeUsage(function.getFullyQualifiedName(), type.getOffset(), true));
                    } else {
                        result.add(type);
                    }
                }
            } else {
                result.add(new TypeUsage(type.getType().replace(name, function.getFullyQualifiedName()), type.getOffset(), false));
            }
        } else {
            result.add(type);
        }
        return result;
    }
    
    /**
     * 
     * @param object
     * @param chain
     * @return 
     */
    private static Collection<TypeUsage> resolveSemiTypeChain(JsObject object, String chain) {
        Collection<TypeUsage> result = new HashSet<TypeUsage>();
        if (chain.isEmpty()) {
            return result;
        }
        if (PROTOTYPE.equals(object.getName())) {
            object = object.getParent();
            if (object == null) {
                return result;
            }
        }
        String[] parts = chain.substring(1).split(SemiTypeResolverVisitor.ST_START_DELIMITER);
        JsObject resultObject = null;
        JsObject testObject = object;
        String kind = "";   //NOI18N
        String name;
        for (String part : parts) {
            int index = part.indexOf(";");  //NOI18N
            if (index > 0) {
                kind = part.substring(0, index);
                name = part.substring(index + 1);
                resultObject = testObject.getProperty(name);
                if (resultObject == null) {
                    JsObject prototype = testObject.getProperty(PROTOTYPE);
                    if (prototype != null) {
                        resultObject = prototype.getProperty(name);
                    }
                }
                if (resultObject == null) {
                    break;
                }
                testObject = resultObject;
            }
            else {
                break;
            }
        }
        if (resultObject != null) {
            if (resultObject instanceof JsFunction) {
                if ("call".endsWith(kind)) {
                    ModelUtils.addUniqueType(result, (Collection<TypeUsage>)((JsFunction)resultObject).getReturnTypes());
                } else {
                    ModelUtils.addUniqueType(result, new TypeUsage(resultObject.getFullyQualifiedName(), -1, true));
                }
            }else {
                Collection<? extends TypeUsage> assignments = resultObject.getAssignments();
                if (assignments.isEmpty()) {
                    ModelUtils.addUniqueType(result, new TypeUsage(resultObject.getFullyQualifiedName(), -1, true));
                } else {
                    ModelUtils.addUniqueType(result, (Collection<TypeUsage>)resultObject.getAssignments());
                }
            }
        }
        return result;
    }
    
    public static Collection<TypeUsage> resolveTypeFromExpression (Model model, @NullAllowed Index jsIndex, List<String> exp, int offset, boolean includeAllPossible) {
        List<JsObject> localObjects = new ArrayList<JsObject>();
        List<JsObject> lastResolvedObjects = new ArrayList<JsObject>();
        List<TypeUsage> lastResolvedTypes = new ArrayList<TypeUsage>();
        
            for (int i = exp.size() - 1; i > -1; i--) {
                String kind = exp.get(i);
                String name = exp.get(--i);
                if (name.startsWith("@ano:")){
                    String[] parts = name.split(":");
                    int anoOffset = Integer.parseInt(parts[1]);
                    JsObject anonym = ModelUtils.findJsObject(model, anoOffset);
                    lastResolvedObjects.add(anonym);
                    continue;
                }
                if (ModelUtils.THIS.equals(name)) {
                    JsObject thisObject = ModelUtils.findJsObject(model, offset);
                    JsObject first = thisObject;
                    while (thisObject != null && thisObject.getParent() != null && thisObject.getParent().getJSKind() != JsElement.Kind.FILE
                            && thisObject.getJSKind() != JsElement.Kind.CONSTRUCTOR
                            && thisObject.getJSKind() != JsElement.Kind.ANONYMOUS_OBJECT
                            && thisObject.getJSKind() != JsElement.Kind.OBJECT_LITERAL) {
                        thisObject = thisObject.getParent();
                    }
                    if ((thisObject == null || thisObject.getParent() == null) && first != null) {
                        thisObject = first;
                    }
                    if (thisObject != null) {
                        name = thisObject.getName();
                    }
                }
                if (i == (exp.size() - 2)) {
                    JsObject localObject = null;
                    // resolving the first part of expression
                    // find possible variables from local context, index contains only 
                    // public definition, we are interested in the private here as well
                    int index = name.lastIndexOf('.');
                    // needs to look, whether the expression is in a with statement
                    Collection<? extends TypeUsage> typeFromWith = getTypeFromWith(model, offset);
                    if (!typeFromWith.isEmpty()) {
                        String firstNamePart = index == -1 ? name : name.substring(0, index);
                        String changedName = name;
                        for (TypeUsage type : typeFromWith) {
                            //Collection<TypeUsage> resolveTypeFromSemiType = ModelUtils.resolveTypeFromSemiType(model.getGlobalObject(), type);
                            String sType = type.getType();
//                            if (sType.startsWith("@exp;")) {
//                                sType = sType.substring(5);
//                                sType = sType.replace("@pro;", ".");
//                            }
                            localObject = ModelUtils.findJsObjectByName(model, sType);
                            if (localObject != null && localObject.getProperty(firstNamePart) != null) {
                                changedName = localObject.getFullyQualifiedName() + "." + name;
                            } else {
                                lastResolvedTypes.add(new TypeUsage(sType + "." + name, -1, true));
                            }
                        }
                        name = changedName;
                    }
                    
                    if (index > -1) { // the first part is a fqn
                        localObject = ModelUtils.findJsObjectByName(model, name);
                        if (localObject != null) {
                            localObjects.add(localObject);
                        }
                    } else {
                        boolean canBeWindowsProp = true;
                        for (JsObject object : model.getVariables(offset)) {
                            if (object.getName().equals(name)) {
                                localObjects.add(object);
                                localObject = object;
                                break;
                            }
                        }
                        if (localObject != null && localObject.getJSKind().isFunction() && i - 2 > -1) {
                            JsFunction localFunc = (JsFunction)localObject;
                            String paramName = exp.get(i - 2);
                            if (localFunc.getParameter(paramName) != null) {
                                canBeWindowsProp = false;
                            }
                        }

                        for (JsObject libGlobal : ModelExtender.getDefault().getExtendingGlobalObjects(model.getGlobalObject().getFileObject())) {
                            assert libGlobal != null;
                            for (JsObject object : libGlobal.getProperties().values()) {
                                if (object.getName().equals(name)) {
                                    //localObjects.add(object);
                                    lastResolvedTypes.add(new TypeUsage(object.getName(), -1, true));
                                    break;
                                }
                            }
                        }
                        if (jsIndex != null && canBeWindowsProp) {
                            Collection<TypeUsage> windowProperty = tryResolveWindowProperty(model, jsIndex, name);
                            if (windowProperty != null && !windowProperty.isEmpty()) {
                                lastResolvedTypes.addAll(windowProperty);
                            }
                        }
                    }
                    if(localObject == null || (localObject.getJSKind() != JsElement.Kind.PARAMETER
                            && (ModelUtils.isGlobal(localObject.getParent()) || localObject.getJSKind() != JsElement.Kind.VARIABLE))) {
                        // Add global variables from index
//                        Collection<IndexedElement> globalVars = jsIndex.getGlobalVar(name);
//                        for (IndexedElement globalVar : globalVars) {
//                            if(name.equals(globalVar.getName())) {
//                                Collection<TypeUsage> assignments = globalVar.getAssignments();
//                                if (assignments.isEmpty()) {
//                                    lastResolvedTypes.add(new TypeUsage(name, -1, true));
//                                } else {
//                                    lastResolvedTypes.addAll(assignments);
//                                    }
//                        }
//                    }
                        List<TypeUsage> fromAssignments = new ArrayList<TypeUsage>();
//                        if (localObject != null) {
//                            //make it only for the right offset
//                            for(TypeUsage type: localObject.getAssignmentForOffset(offset)) {
//                                resolveAssignments(jsIndex, type.getType(), fromAssignments);
//                            }
//                        } else {
                        if ("@pro".equals(kind) && jsIndex != null) { //NOI18N
                            resolveAssignments(model, jsIndex, name, -1,  fromAssignments);
                        } 
//                        }
                        lastResolvedTypes.addAll(fromAssignments);
                        if (!typeFromWith.isEmpty()) {
//                            Collection<TypeUsage> resolveTypes = ModelUtils.resolveTypes(typeFromWith, parserRestult);
                            
                            for (TypeUsage typeUsage : typeFromWith) {
                                String sType = typeUsage.getType();
                                if (sType.startsWith("@exp;")) {
                                    sType = sType.substring(5);
                                    sType = sType.replace("@pro;", ".");
                                }   
                                ModelUtils.resolveAssignments(model, jsIndex, sType, typeUsage.getOffset(), fromAssignments);
                                for (TypeUsage typeUsage1 : fromAssignments) {
                                    String localFqn = localObject != null ? localObject.getFullyQualifiedName() : null;
                                    if (localFqn != null  && name.startsWith(localFqn) && name.length() > localFqn.length() ) {
                                        lastResolvedTypes.add(new TypeUsage(typeUsage1.getType() + kind + ";" + name.substring(localFqn.length() + 1), typeUsage.getOffset(), false));
                                    } else {
                                        if (!typeUsage1.getType().equals(name)) {
                                            lastResolvedTypes.add(new TypeUsage(typeUsage1.getType() + kind + ";" + name, typeUsage.getOffset(), false));
                                        } else {
                                            lastResolvedTypes.add(typeUsage1);
                                        }
                                    }
                                }
                                
                            }
                        }
                    }
                    
                    if(!localObjects.isEmpty()){
                        for(JsObject lObject : localObjects) {
                            if(lObject.getAssignmentForOffset(offset).isEmpty()) {
                                boolean addAsType = lObject.getJSKind() == JsElement.Kind.OBJECT_LITERAL;
                                if (lObject instanceof JsObjectReference) {
                                    // translate reference objects to the original objects / type
                                    JsObject original = ((JsObjectReference)lObject).getOriginal();
                                    if (original != null){
                                        name = original.getDeclarationName() != null ? original.getDeclarationName().getName() : original.getName();
                                    }
                                }
                                if(addAsType) {
                                    // here it doesn't have to be real type, it's possible that it's just an object name
                                    lastResolvedTypes.add(new TypeUsage(name, -1, true));
                                }
                            }
                            if ("@mtd".equals(kind)) {  //NOI18N
                                if (lObject.getJSKind().isFunction()) {
                                    // if it's a method call, add all retuturn types
                                    lastResolvedTypes.addAll(((JsFunction) lObject).getReturnTypes());
                                }
                                int lastCallOffset = -1;
                                for (Occurrence occurrence : lObject.getOccurrences()) {
                                    if (lastCallOffset < occurrence.getOffsetRange().getStart() && occurrence.getOffsetRange().getStart() <= offset) {
                                        lastCallOffset = occurrence.getOffsetRange().getStart();
                                    }
                                }
                                Collection<TypeUsage> returnTypesFromFrameworks = model.getReturnTypesFromFrameworks(lObject.getName(), lastCallOffset);
                                if (returnTypesFromFrameworks != null && !returnTypesFromFrameworks.isEmpty()) {
                                    lastResolvedTypes.addAll(returnTypesFromFrameworks);
                                }
                                if (jsIndex != null) {
                                    Collection<? extends IndexResult> findByFqn = jsIndex.findByFqn(name, Index.TERMS_BASIC_INFO);
                                    for (Iterator<? extends IndexResult> iterator = findByFqn.iterator(); iterator.hasNext();) {
                                        IndexedElement indexElement = IndexedElement.create(iterator.next());
                                        if(indexElement instanceof IndexedElement.FunctionIndexedElement) {
                                            IndexedElement.FunctionIndexedElement iFunction = (IndexedElement.FunctionIndexedElement)indexElement;
                                            for (String type : iFunction.getReturnTypes()) {
                                                lastResolvedTypes.add(new TypeUsage(type, -1, false));
                                            }
                                        }

                                    }
                                }
                            } else if ("@arr".equals(kind) && lObject instanceof JsArray) {
                                lastResolvedTypes.addAll(((JsArray) lObject).getTypesInArray());
                            } else {
                                // just property
                                 Collection<? extends Type> lastTypeAssignment = lObject.getAssignmentForOffset(offset);
                                // we need to process the object later anyway. To get learning cc, see issue #224453
                                lastResolvedObjects.add(lObject);
                                if (!lastTypeAssignment.isEmpty()) {
                                    // go through the assignments and find the last object / type in the assignment chain
                                    // it solve assignements like a = b; b = c; c = d;. the result for a should be d.
                                    resolveAssignments(model, lObject, offset, lastResolvedObjects, lastResolvedTypes);
                                    break;
                                }
                            }
                        }
                    } 
                    // now we should have collected possible local objects
                    // also objects from index, that fits the first part of the expression
                } else {
                    List<JsObject> newResolvedObjects = new ArrayList<JsObject>();
                    List<TypeUsage> newResolvedTypes = new ArrayList<TypeUsage>();
                    for (JsObject localObject : lastResolvedObjects) {
                        // go through the loca object and try find the method / property from the next expression part
                        JsObject property = ((JsObject) localObject).getProperty(name);
                        if (property != null) {
                            if ("@mtd".equals(kind)) {  //NOI18N
                                if (property.getJSKind().isFunction()) {
                                    //Collection<TypeUsage> resovledTypes = resolveTypeFromSemiType(model, property, ((JsFunction) property).getReturnTypes());
                                    Collection<? extends TypeUsage> resovledTypes = ((JsFunction) property).getReturnTypes();
                                    newResolvedTypes.addAll(resovledTypes);
                                }
                            } else if ("@arr".equals(kind)) {
                                if (property instanceof JsArray) {
                                    newResolvedTypes.addAll(((JsArray) property).getTypesInArray());
                                }
                            } else {
                                Collection<? extends TypeUsage> lastTypeAssignment = property.getAssignmentForOffset(offset);
                                if (lastTypeAssignment.isEmpty()) {
                                    newResolvedObjects.add(property);
                                } else {
                                    newResolvedTypes.addAll(lastTypeAssignment);
                                    if(!property.getProperties().isEmpty()) {
                                        newResolvedObjects.add(property);
                                    }
                                }
                            }
                        }
                    }
                    
                    
                    
                    for (TypeUsage typeUsage : lastResolvedTypes) {
                        if (jsIndex != null) {
                            // for the type build the prototype chain.
                            Collection<String> prototypeChain = new ArrayList<String>();
                            String typeName = typeUsage.getType();
                            if (typeName.contains(SemiTypeResolverVisitor.ST_EXP)) {
                                typeName = typeName.substring(typeName.indexOf(SemiTypeResolverVisitor.ST_EXP) + SemiTypeResolverVisitor.ST_EXP.length());
                            }
                            if (typeName.contains(SemiTypeResolverVisitor.ST_PRO)) {
                                typeName = typeName.replace(SemiTypeResolverVisitor.ST_PRO, ".");
                            }
                            prototypeChain.add(typeName);
                            prototypeChain.addAll(findPrototypeChain(typeName, jsIndex));

                            Collection<? extends IndexResult> indexResults = null;
                            String propertyToCheck = null;
                            for (String fqn : prototypeChain) {
                                // at first look at the properties of the object
                                propertyToCheck = fqn + "." + name;
                                indexResults = jsIndex.findByFqn(propertyToCheck,
                                        Index.FIELD_FLAG, Index.FIELD_RETURN_TYPES, Index.FIELD_ARRAY_TYPES, Index.FIELD_ASSIGNMENTS); //NOI18N
                                
                                if (indexResults.isEmpty() && !fqn.endsWith(".prototype")) {
                                    // if the property was not found, try to look at the prototype of the object
                                    propertyToCheck = fqn + ".prototype." + name;
                                    indexResults = jsIndex.findByFqn(propertyToCheck,
                                            Index.FIELD_FLAG, Index.FIELD_RETURN_TYPES, Index.FIELD_ARRAY_TYPES, Index.FIELD_ASSIGNMENTS); //NOI18N
                                }
                                if(!indexResults.isEmpty()) {
                                    // if the property / method was already found, we don't need to continue.
                                    // in the runtime is also used the first one that is found in the prototype chain
                                    break;
                                }
                                propertyToCheck = null;
                            }

                            boolean checkProperty = (indexResults == null || indexResults.isEmpty()) && !"@mtd".equals(kind);
                            if (indexResults != null) {
                                for (IndexResult indexResult : indexResults) {
                                    // go through the resul from index and add appropriate types to the new resolved
                                    JsElement.Kind jsKind = IndexedElement.Flag.getJsKind(Integer.parseInt(indexResult.getValue(Index.FIELD_FLAG)));
                                    if ("@mtd".equals(kind) && jsKind.isFunction()) {
                                        //Collection<TypeUsage> resolved = resolveTypeFromSemiType(model, ModelUtils.findJsObject(model, offset), IndexedElement.getReturnTypes(indexResult));
                                        Collection<TypeUsage> resolvedTypes = IndexedElement.getReturnTypes(indexResult);
                                        ModelUtils.addUniqueType(newResolvedTypes, resolvedTypes);
                                    } else if ("@arr".equals(kind)) { // NOI18N
                                        Collection<TypeUsage> resolvedTypes = IndexedElement.getArrayTypes(indexResult);
                                        ModelUtils.addUniqueType(newResolvedTypes, resolvedTypes);
                                    } else {
                                        checkProperty = true;
                                    }
                                }
                            }
                            if (checkProperty) {
                                String propertyFQN = propertyToCheck != null ? propertyToCheck : typeName + "." + name;
                                List<TypeUsage> fromAssignment = new ArrayList<TypeUsage>();
                                resolveAssignments(model, jsIndex, propertyFQN, -1, fromAssignment);
                                if (fromAssignment.isEmpty()) {
                                    ModelUtils.addUniqueType(newResolvedTypes, new TypeUsage(propertyFQN));
                                } else {
                                    ModelUtils.addUniqueType(newResolvedTypes, fromAssignment);
                                }
                            }
                        }
                        // from libraries look for top level types
                        for (JsObject libGlobal : ModelExtender.getDefault().getExtendingGlobalObjects(model.getGlobalObject().getFileObject())) {
                            for (JsObject object : libGlobal.getProperties().values()) {
                                if (object.getName().equals(typeUsage.getType())) {
                                    JsObject property = object.getProperty(name);
                                    if (property != null) {
                                        JsElement.Kind jsKind = property.getJSKind();
                                        if ("@mtd".equals(kind) && jsKind.isFunction()) {
                                            newResolvedTypes.addAll(((JsFunction) property).getReturnTypes());
                                        } else {
                                            newResolvedObjects.add(property);
                                        }
                                    }
                                    break;
                                }
                            }
                        }
                    }

                    lastResolvedObjects = newResolvedObjects;
                    lastResolvedTypes = newResolvedTypes;
                }
            }
            
            HashMap<String, TypeUsage> resultTypes  = new HashMap<String, TypeUsage> ();
            for (TypeUsage typeUsage : lastResolvedTypes) {
                if(!resultTypes.containsKey(typeUsage.getType())) {
                    resultTypes.put(typeUsage.getType(), typeUsage);
                }
            }
            for (JsObject jsObject : lastResolvedObjects) {
                if (jsObject.isDeclared()) {
                    String fqn = jsObject.getFullyQualifiedName();
                    if (!resultTypes.containsKey(fqn)) {
                        if (includeAllPossible || hasDeclaredProperty(jsObject)) {
                            resultTypes.put(fqn, new TypeUsage(fqn, offset));
                        }
                    }
                }
            }
            return resultTypes.values();
    }

    public static boolean hasDeclaredProperty(JsObject jsObject) {
        boolean result =  false;
        
        Iterator<? extends JsObject> it = jsObject.getProperties().values().iterator();
        while (!result && it.hasNext()) {
            JsObject property = it.next();
            result = property.isDeclared();
            if (!result) {
                result = hasDeclaredProperty(property);
            } 
        }

        return result;
    }
    
    public static List<String> expressionFromType(TypeUsage type) {
        String sexp = type.getType();
        if ((sexp.startsWith("@exp;") || sexp.startsWith("@new;") || sexp.startsWith("@arr;") || sexp.contains("@pro;")
                || sexp.startsWith("@call;") || sexp.startsWith(SemiTypeResolverVisitor.ST_WITH)) && (sexp.length() > 5)) {
            
            if (sexp.charAt(0) == '@') {
                int start = sexp.startsWith("@call;") || sexp.startsWith("@arr;") || sexp.startsWith(SemiTypeResolverVisitor.ST_WITH) ? 1 : sexp.charAt(5) == '@' ? 6 : 5;
                sexp = sexp.substring(start);
            }
            List<String> nExp = new ArrayList<String>();
            String[] split = sexp.split("@");
            for (int i = split.length - 1; i > -1; i--) {
                nExp.add(split[i].substring(split[i].indexOf(';') + 1));
                if (split[i].startsWith("arr;")) {
                    nExp.add("@arr");
                } else if (split[i].startsWith("call;")) {
                    nExp.add("@mtd");
                } else if (split[i].startsWith("with;")) {
                    nExp.add("@with");
                }else {
                    nExp.add("@pro");
                }
            }
            return nExp;
        } else {
            return Collections.singletonList(type.getType());
        }
    }

    /**
     * Resolve assignments to return a resolved typeUsage
     *
     * @param parent Parent Object
     * @param typeHere TypeUsage for the assignment
     * @return
     */
    public static TypeUsage createResolvedType(JsObject parent, TypeUsage typeHere) {
        int invokeCount = 0;
        String fqn = getFQNFromType(typeHere);
        List<TypeUsage> alreadyResolved = new ArrayList<>(); 
        return resolveTypes(parent, fqn, typeHere.getOffset(), alreadyResolved, invokeCount);
    }

    /* @return TypeUsage with generated typename string 
     */
    private static TypeUsage resolveTypes(JsObject parent, String fqn, int offset, List<TypeUsage> alreadyResolved, int invokeCount) {

        invokeCount++;
        String name = fqn;
        StringBuilder props = new StringBuilder();
        int indx = fqn.indexOf(".");
        if (indx != -1) {
            name = fqn.substring(0, indx);
            props.append(fqn.substring(indx + 1));
        }
        List<TypeUsage> localResolved = new ArrayList<>();

        resolveAssignments(parent, name, offset, localResolved, props);

        List<TypeUsage> diff = localResolved.stream().filter(type -> !alreadyResolved.contains(type)).collect(Collectors.toList());
        if (diff.size() > 0) {
            alreadyResolved.addAll(diff);
            boolean typeResolved = false;
            for (TypeUsage type : localResolved) {
                if (type.isResolved()) {
                    String newObjectName = type.getType();
                    JsObject object = ModelUtils.searchJsObjectByName(parent, newObjectName);
                    if ((object != null) && (object != parent)) {
                        String partfqn = props.toString();
                        if (!partfqn.trim().equals("")) {
                            String[] tokens = partfqn.split("\\.");
                            for (int i = 0; i < tokens.length; i++) {
                                object = ModelUtils.searchJsObjectByName(parent, newObjectName);
                                for (JsObject prop : object.getProperties().values()) {
                                    if ((prop.getName().equals(tokens[i])) && (prop.isDeclared())) {
                                        if (prop.getAssignmentCount() > 0) {
                                            if (invokeCount == MAX_RECURSION_DEEP_RESOLVING_ASSIGNMENTS) {
                                                LOG.log(Level.WARNING, "StackOverFlowError : {0} : {1}",
                                                        new Object[]{object.getFullyQualifiedName(),
                                                            object.getFileObject()});
                                            }
                                            return resolveTypes(object, String.join(".", Arrays.copyOfRange(tokens, i, tokens.length)), offset, alreadyResolved, invokeCount);
                                        } else {
                                            object = prop;
                                            newObjectName = newObjectName + "." + prop.getName();
                                            if (i == tokens.length - 1) {
                                                typeResolved = true;
                                            }
                                            break;
                                        }
                                    }
                                }
                            }
                            if (typeResolved) {
                                return new TypeUsage(newObjectName, type.getOffset(), true);
                            }

                        }
                    }
                }
            }
        }
        return new TypeUsage(fqn, offset, false);
    }

    private static void resolveAssignments(JsObject jsObject, String fqn, int offset, List<TypeUsage> resolved, StringBuilder nestedProperties) {

        int invokeCount = 0;
        Set<String> alreadyProcessed = new HashSet<String>();
        for (TypeUsage type : resolved) {
            alreadyProcessed.add(type.getType());
        }
        resolveAssignments(jsObject, fqn, offset, resolved, alreadyProcessed, nestedProperties, invokeCount);
    }

    /**
     * Resolves assignments for the top level object in a given fqn
     *
     * @param parent parent JsObject
     * @param fqn assignment Type name string
     * @param offset assignment Type offset
     * @param resolved list of resolved TypeUsages
     * @param alreadyProcessed List of FQNs already resolved
     * @param nestedProperties "." separated nested property names string part
     * of FQN
     */
    private static void resolveAssignments(JsObject parent, String fqn, int offset, List<TypeUsage> resolved, Set<String> alreadyProcessed, StringBuilder nestedProperties, int invokeCount) {
        if (!alreadyProcessed.contains(fqn)) {
            invokeCount++;
            alreadyProcessed.add(fqn);
            String fqnCorrected = ModelUtils.getFQNFromType(new TypeUsage(fqn, offset, false));
            //resolve the parent object in fqn
            int index = fqnCorrected.indexOf(".");//NOI18N
            if (index != -1) {
                //save the remaining fqn part to locate in object properties later
                nestedProperties.insert(0, fqnCorrected.substring(index + 1) + ".");
                fqnCorrected = fqnCorrected.substring(0, index);
            }
            if (!fqnCorrected.startsWith("@")) {
                List<TypeUsage> toProcess = new ArrayList<TypeUsage>();
                JsObject object = ModelUtils.searchJsObjectByName(parent, fqnCorrected);

                if ((object != null) && (((JsObjectImpl) object).getAssignmentCount() > 0)) {
                    for (TypeUsage type : ((JsObjectImpl) object).getAssignments()) {
                        if (!type.isResolved()) {
                            for (TypeUsage resolvedType : resolveTypeFromSemiType(object, type)) {
                                toProcess.add(resolvedType);
                            }
                        } else {
                            toProcess.add(type);
                        }
                    }
                    for (TypeUsage type : toProcess) {
                        if (!alreadyProcessed.contains(type.getType())) {
                            if (invokeCount == MAX_RECURSION_DEEP_RESOLVING_ASSIGNMENTS) {
                                LOG.log(Level.WARNING, "StackOverFlowError : {0} : {1}",
                                        new Object[]{object.getFullyQualifiedName(),
                                                     object.getFileObject()});
                            }
                            resolveAssignments(parent, type.getType(), type.getOffset(), resolved, alreadyProcessed, nestedProperties, invokeCount);
                        }
                    }
                } else {
                    ModelUtils.addUniqueType(resolved, new TypeUsage(fqnCorrected, offset, true));
                }
            } else {
                ModelUtils.addUniqueType(resolved, new TypeUsage(fqnCorrected, offset, false));
            }
        }
    }

    private static JsObject searchJsObjectByName(JsObject parent, String fqn) {
        JsObject object = ModelUtils.findJsObjectByName(parent, fqn);
        if (object == null) {
            if (!isGlobal(parent)) {
                if (parent.getParent() != null) {
                    parent = parent.getParent();
                    return searchJsObjectByName(parent, fqn);
                }
            }
        }
        return object;
    }
    
    public static Collection<TypeUsage> resolveTypes(Collection<? extends TypeUsage> unresolved, Model model, Index jsIndex, boolean includeAllPossible) {
        //assert !SwingUtilities.isEventDispatchThread() : "Type resolution may block AWT due to index search";
        Collection<TypeUsage> types = new ArrayList<TypeUsage>(unresolved);
        if (types.size() == 1 && types.iterator().next().isResolved()) {
            return types;
        }
        Set<String> original = null;
        int cycle = 0;
        boolean resolvedAll = false;
        while (!resolvedAll && cycle < 10) {
            cycle++;
            resolvedAll = true;
            Collection<TypeUsage> resolved = new ArrayList<TypeUsage>();
            for (TypeUsage typeUsage : types) {
                if (!typeUsage.isResolved()) {
                    if (original == null) {
                        original = new HashSet<String>(unresolved.size());
                        for (TypeUsage t : unresolved) {
                            original.add(t.getType());
                        }
                    }
                    resolvedAll = false;
                    List<String> nExp = expressionFromType(typeUsage);
                    if (nExp.size() > 1) {
                        // passing original prevents the unresolved return types
                        // when recursion in place
                        ModelUtils.addUniqueType(resolved, original, ModelUtils.resolveTypeFromExpression(model, jsIndex, nExp, typeUsage.getOffset(), includeAllPossible));
                    } else {
                        ModelUtils.addUniqueType(resolved, new TypeUsage(typeUsage.getType(), typeUsage.getOffset(), true));
                    }
                } else {
                    ModelUtils.addUniqueType(resolved, (TypeUsage) typeUsage);
                }
            }
            types.clear();
            types = new ArrayList<TypeUsage>(resolved);
        }
        return types;
    }
    
    private static void resolveAssignments(Model model, JsObject jsObject, int offset, List<JsObject> resolvedObjects, List<TypeUsage> resolvedTypes) {
        Collection<? extends TypeUsage> assignments = jsObject.getAssignmentForOffset(offset);
        for (TypeUsage typeName : assignments) {
            if (typeName.isResolved()) {
                resolvedTypes.add(typeName);
                continue;
            }
            String type = typeName.getType();
            if (type.startsWith(SemiTypeResolverVisitor.ST_WITH)) {
                List<String> expression = expressionFromType((TypeUsage)typeName);
                Collection<? extends TypeUsage> typesFromWith = ModelUtils.getTypeFromWith(model, typeName.getOffset());
                expression.remove(expression.size() - 1);
                expression.remove(expression.size() - 1);

                StringBuilder sb = new StringBuilder();
                for (int i = expression.size() - 1; i > 0; i--) {
                    sb.append(expression.get(i--));
                    sb.append(";");
                    sb.append(expression.get(i));
                }
                for (TypeUsage typeWith: typesFromWith) {
                    resolvedTypes.add(new TypeUsage(SemiTypeResolverVisitor.ST_EXP + typeWith.getType() + sb.toString(), typeName.getOffset(), false));
                }
                resolvedTypes.add(new TypeUsage(sb.toString(), typeName.getOffset(), false));
                
            } else {
                JsObject byOffset = findObjectForOffset(typeName.getType(), offset, model);
                if (byOffset != null) {
                    if(!jsObject.getName().equals(byOffset.getName())) {
                        resolvedObjects.add(byOffset);
                        resolveAssignments(model, byOffset, offset, resolvedObjects, resolvedTypes);
                    }
                } else {
                    resolvedTypes.add((TypeUsage)typeName);
                }
            }
        }
    }
    
    private static int deepRA = 0;
    private static void resolveAssignments(Model model, Index jsIndex, String fqn, int offset, List<TypeUsage> resolved) {
        Set<String> alreadyProcessed = new HashSet<String>();
        deepRA = 0;
        for(TypeUsage type : resolved) {
            alreadyProcessed.add(type.getType());
        }
        resolveAssignments(model, jsIndex, fqn, offset, resolved, alreadyProcessed);
    }
    
    private static void resolveAssignments(Model model, Index jsIndex, String fqn, int offset,  List<TypeUsage> resolved, Set<String> alreadyProcessed) {
        if (!alreadyProcessed.contains(fqn)) {
            alreadyProcessed.add(fqn);
            deepRA++;
            String fqnCorrected = fqn;
            if (fqnCorrected.startsWith(SemiTypeResolverVisitor.ST_EXP) && !fqnCorrected.contains(SemiTypeResolverVisitor.ST_CALL)) {
                fqnCorrected = fqnCorrected.substring(fqnCorrected.indexOf(SemiTypeResolverVisitor.ST_EXP) + SemiTypeResolverVisitor.ST_EXP.length());
                fqnCorrected = fqnCorrected.replace(SemiTypeResolverVisitor.ST_PRO, ".");   //NOI18N
            }
            if (!fqnCorrected.startsWith("@")) {
                if (jsIndex != null) {
                    Collection<? extends IndexResult> indexResults = jsIndex.findByFqn(fqnCorrected, Index.FIELD_ASSIGNMENTS);
                    boolean hasAssignments = false;
                    boolean isType = false;
                    for (IndexResult indexResult: indexResults) {
                        Collection<TypeUsage> assignments = IndexedElement.getAssignments(indexResult);
                        if (!assignments.isEmpty()) {
                            hasAssignments = true;
                            for (TypeUsage type : assignments) {
                                if (resolved.size() > 10) {
                                    resolved.clear();
                                    break;
                                }
                                if (!alreadyProcessed.contains(type.getType()) && deepRA < MAX_RECURSION_DEEP_RESOLVING_ASSIGNMENTS) {
                                    resolveAssignments(model, jsIndex, type.getType(), type.getOffset(), resolved, alreadyProcessed);
                                }
                            }
                        }
                    }
                    if (indexResults.isEmpty()) {
                        JsObject found = ModelUtils.findJsObjectByName(model.getGlobalObject(), fqnCorrected);
                        if (found != null) {
                            Collection<? extends TypeUsage> assignments = found.getAssignments();
                            if (!assignments.isEmpty()) {
                                hasAssignments = true;
                                List<TypeUsage> toProcess = new ArrayList<TypeUsage>();
                                for (TypeUsage type : assignments) {
                                    if (!type.isResolved()) {
                                        for (TypeUsage resolvedType : resolveTypeFromSemiType(found, type)) {
                                            toProcess.add(resolvedType);
                                        }
                                    } else {
                                        toProcess.add(type);
                                    }
                                }
                                for (TypeUsage type : toProcess) {
                                    if (!alreadyProcessed.contains(type.getType()) && deepRA < MAX_RECURSION_DEEP_RESOLVING_ASSIGNMENTS) {
                                        resolveAssignments(model, jsIndex, type.getType(), type.getOffset(), resolved, alreadyProcessed);
                                    }
                                }
                            }
                        }
                    }

                    Collection<IndexedElement> properties = jsIndex.getProperties(fqnCorrected);
                    for (IndexedElement property : properties) {
                        if (property.getFQN().startsWith(fqnCorrected) && (property.isDeclared() || ModelUtils.PROTOTYPE.equals(property.getName()))) {
                            isType = true;
                            break;
                        }
                    }


                    if(!hasAssignments || isType) {
                        ModelUtils.addUniqueType(resolved, new TypeUsage(fqnCorrected, offset, true));
                    }
                }
            } else {
                ModelUtils.addUniqueType(resolved, new TypeUsage(fqn, offset, false));
            }
        }
    }
    
    public static JsObject findObjectForOffset(String name, int offset, Model model) {
        for (JsObject object : model.getVariables(offset)) {
            if (object.getName().equals(name)) {
                return object;
            }
        }
        return null;
    }

    public static Collection<String> findPrototypeChain(String fqn, Index jsIndex) {
        Collection<String> chain = findPrototypeChain(fqn, jsIndex, new HashSet<String>());
        return chain;
    }

    private static Collection<String> findPrototypeChain(String fqn, Index jsIndex, Set<String> alreadyCheck) {
        Collection<String> result = new HashSet<>();
        if (!alreadyCheck.contains(fqn)) {
            alreadyCheck.add(fqn);
            Collection<? extends IndexResult> indexResults = jsIndex.findByFqn(fqn + "." + ModelUtils.PROTOTYPE, Index.FIELD_ASSIGNMENTS); //NOI18N
            for (IndexResult indexResult : indexResults) {
                Collection<TypeUsage> assignments = IndexedElement.getAssignments(indexResult);
                for (TypeUsage typeUsage : assignments) {
                    result.add(typeUsage.getType());
                }
                for (TypeUsage typeUsage : assignments) {
                    result.addAll(findPrototypeChain(typeUsage.getType(), jsIndex, alreadyCheck));
                }
            }
        } 
        if (result.isEmpty()) {
            result.add("Object"); //NOI18N
        }
        return result;
    }
    
    /**
     * 
     * @param model
     * @param offset
     * @return types from with expressions. The collection has the order of items from most inner with to
     *      the outer with.
     */
    public static Collection <? extends TypeUsage> getTypeFromWith(Model model, int offset) {
        JsObject jsObject = ModelUtils.findJsObject(model, offset);
        JsObject previous = jsObject;
        while (jsObject != null && jsObject.isAnonymous() && jsObject.getJSKind() != JsElement.Kind.WITH_OBJECT) {
            jsObject = ModelUtils.findJsObject(model, jsObject.getOffset() - 1);
            if (jsObject.getFullyQualifiedName().endsWith(previous.getFullyQualifiedName())) {
                break;
            } else {
                previous = jsObject;
            }
        }
        while(jsObject != null && jsObject.getJSKind() != JsElement.Kind.WITH_OBJECT) {
            jsObject = jsObject.getParent();
        }
        if (jsObject != null && jsObject.getJSKind() == JsElement.Kind.WITH_OBJECT) {
            List<TypeUsage> types = new ArrayList<TypeUsage>();
            JsWith wObject = (JsWith)jsObject;
            Collection<? extends TypeUsage> withTypes = wObject.getTypes();
            types.addAll(withTypes);
            while (wObject.getOuterWith() != null) {
                wObject = wObject.getOuterWith();
                withTypes = wObject.getTypes();
                types.addAll(withTypes);
            }
            return types;
        }
        return Collections.EMPTY_LIST;
    }

    public static Collection<Identifier> getDefinedGlobal(final Snapshot snapshot, final int offset) {
        ArrayList<Identifier> names = new ArrayList<Identifier>();
        List<JsTokenId> findToken = Arrays.asList(JsTokenId.BLOCK_COMMENT);
        TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(snapshot, offset);
        if (ts == null) {
            return names;
        }
        ts.move(0);
        Token<? extends JsTokenId> token = LexUtilities.findNextIncluding(ts, findToken);
        while (token != null && token.id() == JsTokenId.BLOCK_COMMENT) {
            int iOffset = ts.offset();
            String text = token.text().toString();
            // cut /*
            text = text.substring(2);
            iOffset += 2;
            while (!text.isEmpty() && text.charAt(0) == ' ') {
                text = text.substring(1);
                iOffset++;
            }
            if (text.startsWith(GLOBAL_DIRECTIVE) && text.length() > GLOBAL_DIRECTIVE.length() && text.charAt(GLOBAL_DIRECTIVE.length()) == ' ') {
                text = text.substring(GLOBAL_DIRECTIVE.length() + 1);
                iOffset = iOffset + GLOBAL_DIRECTIVE.length() + 1;
                String[] parts = text.split(",");
                for (String part : parts) {
                    String name = part;
                    int nameOffset = iOffset;
                    while (!name.isEmpty() && name.charAt(0) == ' ') {
                        name = name.substring(1);
                        nameOffset++;
                    }
                    name = name.trim();
                    if (name.indexOf('*') > 0) {
                        name = name.substring(0, name.indexOf('*')).trim();
                    }
                    if (name.indexOf(':') > 0) {
                        name = name.substring(0, name.indexOf(':')).trim();
                    }
                    if (!name.isEmpty()) {
                        names.add(new Identifier(name, new OffsetRange(nameOffset, nameOffset + name.length())));
                    }
                    iOffset = iOffset + part.length() + 1;
                }
            }
            if (ts.moveNext()) {
                token = LexUtilities.findNextIncluding(ts, findToken);
            } else {
                break;
            }
        }
        return names;
    }
    
    public static List<? extends JsObject> getExtendingGlobalObjects(FileObject fo) {
        return ModelExtender.getDefault().getExtendingGlobalObjects(fo);
    }
    
    public static void addUniqueType(Collection <TypeUsage> where, Set<String> forbidden, TypeUsage type) {
        String typeName = type.getType();
        if (forbidden.contains(typeName)) {
            return;
        }
        for (TypeUsage utype : where) {
            if (utype.getType().equals(typeName)) {
                return;
            }
        }
        where.add(type);
    }

    public static void addUniqueType(Collection <TypeUsage> where, TypeUsage type) {
        addUniqueType(where, Collections.<String>emptySet(), type);
    }

    public static void addUniqueType(Collection <TypeUsage> where, Set<String> forbidden, Collection <TypeUsage> what) {
        for (TypeUsage type: what) {
            addUniqueType(where, forbidden, type);
        }
    }

    public static void addUniqueType(Collection <TypeUsage> where, Collection <TypeUsage> what) {
        addUniqueType(where, Collections.<String>emptySet(), what);
    }
    
    
    
    public static void addDocTypesOccurence(JsObject jsObject, JsDocumentationHolder docHolder) {
        if (docHolder.getOccurencesMap().containsKey(jsObject.getFullyQualifiedName())) {
            for (OffsetRange offsetRange : docHolder.getOccurencesMap().get(jsObject.getFullyQualifiedName())) {
                ((JsObjectImpl)jsObject).addOccurrence(offsetRange);
            }
        }
    }
    
    public static String getDisplayName(String typeName) {
        String displayName = typeName;
        if (displayName.startsWith("@param;") || displayName.contains(ModelBuilder.WITH_OBJECT_NAME_START)
                || displayName.contains(ModelBuilder.ANONYMOUS_OBJECT_NAME_START)) {
            displayName = "";
        } else {
            if (displayName.contains(GENERATED_FUNCTION_PREFIX)) {
                displayName = removeGeneratedFromFQN(displayName, GENERATED_FUNCTION_PREFIX);
            }
            if (displayName.contains(GENERATED_ANONYM_PREFIX)) {
                displayName = removeGeneratedFromFQN(displayName, GENERATED_ANONYM_PREFIX);
            }
        }
        return displayName;
    }
    
    public static String getDisplayName(Type type) {
        List<TypeNameConvertor> convertors = ModelExtender.getDefault().getTypeNameConvertors();
        String displayName = null;
        for (TypeNameConvertor convertor: convertors) {
            displayName = convertor.getDisplayName(type);
            if (displayName != null && !displayName.isEmpty() && !displayName.equals(type.getType())) {
                break;
            }
        }
        if (displayName == null || displayName.isEmpty() || displayName.equals(type.getType())) {
            displayName = ModelUtils.getDisplayName(type.getType());
        }
        return displayName;
    }

    /**
     * 
     * @param fqn fully qualified name of the type
     * @param generated the generated prefix
     * @return the fully qualified name without the generated part or empty string if the generated name is the last one.
     * 
     */
    private static String removeGeneratedFromFQN(String fqn, String generated) {
        String[] parts = fqn.split("\\."); //NOI18N
        String part = parts[parts.length - 1];
        if(part.contains(generated)) {
            try {
                Integer.parseInt(part.substring(generated.length()));
                return ""; // return empty name if the last name is generated
            } catch (NumberFormatException nfe) {
                // do nothing
            }
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < parts.length; i++) {
            part = parts[i];
            boolean add = true;
            if (part.startsWith(generated) || (i == 0 && part.contains(generated))) {
//            if (part.startsWith(generated)) {
                try {
                    Integer.parseInt(part.substring(part.indexOf(generated) + generated.length()));
//                    Integer.parseInt(part.substring(generated.length()));
                    add = false;
                } catch (NumberFormatException nfe) {
                    // do nothing
                }
            }
            if (add) {
                sb.append(part);
                if (i < (parts.length - 1)) {
                    sb.append(".");
                }
            }
        }
        return sb.toString();
    }
    
    private static List<String> knownGlobalObjects = Arrays.asList("window", "document", "console",
            "clearInterval", "clearTimeout", "event", "frames", "history",
            "Image", "location", "name", "navigator", "Option", "parent", "screen", "setInterval", "setTimeout",
            "XMLHttpRequest", "JSON", "Date", Type.UNDEFINED, "Math",  //NOI18N
            Type.ARRAY, Type.OBJECT, Type.BOOLEAN, Type.NULL, Type.NUMBER, Type.REGEXP, Type.STRING, Type.UNDEFINED, Type.UNRESOLVED,
            Type.NAN, Type.INFINITY);
    
    public static boolean isKnownGLobalType(String type) {
        return knownGlobalObjects.contains(type);
    }
    
    /**
     * 
     * @param snapshot
     * @param offset offset where the expression should be resolved
     * @param lookBefore if yes, looks for the beginning of the expression before the offset,
     *                  if no, it can be in a middle of expression
     * @return 
     */
    public static List<String> resolveExpressionChain(Snapshot snapshot, int offset, boolean lookBefore) {
        TokenHierarchy<?> th = snapshot.getTokenHierarchy();
        TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(th, offset);
        if (ts == null) {
            return Collections.<String>emptyList();
        }

        ts.move(offset);
        if (ts.movePrevious() && (ts.moveNext() || ((ts.offset() + ts.token().length()) == snapshot.getText().length()))) {
            if (!lookBefore && ts.token().id() != JsTokenId.OPERATOR_DOT) {
                ts.movePrevious();
            }
            Token<? extends JsTokenId> token = lookBefore ? LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.BLOCK_COMMENT, JsTokenId.EOL)) : ts.token();
            int parenBalancer = 0;
            // 1 - method call, 0 - property, 2 - array
            int partType = 0;
            boolean wasLastDot = lookBefore;
            int offsetFirstRightParen = -1;
            List<String> exp = new ArrayList();

            while (token.id() != JsTokenId.OPERATOR_SEMICOLON
                    && token.id() != JsTokenId.BRACKET_RIGHT_CURLY && token.id() != JsTokenId.BRACKET_LEFT_CURLY
                    && token.id() != JsTokenId.BRACKET_LEFT_PAREN
                    && token.id() != JsTokenId.BLOCK_COMMENT
                    && token.id() != JsTokenId.LINE_COMMENT
                    && token.id() != JsTokenId.OPERATOR_ASSIGNMENT
                    && token.id() != JsTokenId.OPERATOR_PLUS) {
                
                if (token.id() == JsTokenId.WHITESPACE) {
                    // we need to find out, whether this is a continual expression on the new line
                    int helpOffset = ts.offset();
                    if (ts.movePrevious()) {
                        token = LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.BLOCK_COMMENT, JsTokenId.LINE_COMMENT, JsTokenId.EOL));
                        if (token.id() != JsTokenId.BRACKET_RIGHT_PAREN && token.id() != JsTokenId.IDENTIFIER
                                && token.id() != JsTokenId.OPERATOR_DOT) {
                            ts.move(helpOffset);
                            ts.moveNext();
                            token = ts.token();
                            break;
                        }
                    }
                }
                if (token.id() != JsTokenId.EOL) {
                    if (token.id() != JsTokenId.OPERATOR_DOT) {
                        if (token.id() == JsTokenId.BRACKET_RIGHT_PAREN) {
                            parenBalancer++;
                            partType = 1;
                            if (offsetFirstRightParen == -1) {
                                offsetFirstRightParen = ts.offset();
                            }
                            while (parenBalancer > 0 && ts.movePrevious()) {
                                token = ts.token();
                                if (token.id() == JsTokenId.BRACKET_RIGHT_PAREN) {
                                    parenBalancer++;
                                } else {
                                    if (token.id() == JsTokenId.BRACKET_LEFT_PAREN) {
                                        parenBalancer--;
                                    }
                                }
                            }
                        } else if (token.id() == JsTokenId.BRACKET_RIGHT_BRACKET) {
                            parenBalancer++;
                            partType = 2;
                            while (parenBalancer > 0 && ts.movePrevious()) {
                                token = ts.token();
                                if (token.id() == JsTokenId.BRACKET_RIGHT_BRACKET) {
                                    parenBalancer++;
                                } else {
                                    if (token.id() == JsTokenId.BRACKET_LEFT_BRACKET) {
                                        parenBalancer--;
                                    }
                                }
                            }
                        } else if (parenBalancer == 0 && "operator".equals(token.id().primaryCategory())) { // NOI18N
                            return exp;
                        } else {
                            exp.add(token.text().toString());
                            switch (partType) {
                                case 0:
                                    exp.add("@pro");   // NOI18N
                                    break;
                                case 1:
                                    exp.add("@mtd");   // NOI18N
                                    offsetFirstRightParen = -1;
                                    break;
                                case 2:
                                    exp.add("@arr");    // NOI18N
                                    break;
                                default:
                                    break;
                            }
                            partType = 0;
                            wasLastDot = false;
                        }
                    } else {
                        wasLastDot = true;
                    }
                } else {
                    if (!wasLastDot && ts.movePrevious()) {
                        // check whether it's continuatino of previous line
                        token = LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.BLOCK_COMMENT, JsTokenId.LINE_COMMENT));
                        if (token.id() != JsTokenId.OPERATOR_DOT) {
                            // the dot was not found => it's not continuation of expression
                            break;
                        }
                    }
                }
                if (!ts.movePrevious()) {
                    break;
                }
                token = ts.token();
            }
            if (token.id() == JsTokenId.WHITESPACE) {
                if (ts.movePrevious()) {
                    token = LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.BLOCK_COMMENT, JsTokenId.EOL));
                    if (token.id() == JsTokenId.KEYWORD_NEW && !exp.isEmpty()) {
                        exp.remove(exp.size() - 1);
                        exp.add("@pro");    // NOI18N
                    } else if (!lookBefore && offsetFirstRightParen > -1) {
                        // in the case when the expression is like ( new Object()).someMethod
                        exp.addAll(resolveExpressionChain(snapshot, offsetFirstRightParen - 1, true));
                    }
                }
            } else if (exp.isEmpty() && !lookBefore && offsetFirstRightParen > -1) {
                // in the case when the expression is like ( new Object()).someMethod
                exp.addAll(resolveExpressionChain(snapshot, offsetFirstRightParen - 1, true));
            } else if (wasLastDot && !lookBefore && token.id() == JsTokenId.BRACKET_RIGHT_CURLY) {
                int balancer = 1;
                while (balancer > 0 && ts.movePrevious()) {
                    token = ts.token();
                    if (token.id() == JsTokenId.BRACKET_RIGHT_CURLY) {
                        balancer++;
                    } else {
                        if (token.id() == JsTokenId.BRACKET_LEFT_CURLY) {
                            balancer--;
                        }
                    }
                }
                exp.add("@ano:" + ts.offset());
                exp.add("@pro");
            }
            return exp;
        }
        return Collections.<String>emptyList();
    }
    
    public static void moveProperty (JsObject newParent, JsObject property) {
        JsObject newProperty = newParent.getProperty(property.getName());
        if (property.getParent() != null) {
            property.getParent().getProperties().remove(property.getName());
        }
        if (newProperty == null) {
            ((JsObjectImpl)property).setParent(newParent);
            newParent.addProperty(property.getName(), property);
        } else {
            if (property.isDeclared() && !newProperty.isDeclared()) {
                JsObject tmpProperty = newProperty;
                newParent.addProperty(property.getName(), property);
                ((JsObjectImpl)property).setParent(newParent);
                newProperty = property;
                property = tmpProperty;
            }
            JsObjectImpl.moveOccurrenceOfProperties((JsObjectImpl) newProperty, property);
            for (Occurrence occurrence : property.getOccurrences()) {
                newProperty.addOccurrence(occurrence.getOffsetRange());
            }
            List<JsObject>propertiesToMove = new ArrayList(property.getProperties().values());
            for (JsObject propOfProperty: propertiesToMove) {
                moveProperty(newProperty, propOfProperty);
            }
        }
    }
    
    /**
     * It change the declaration scope of the input object to the new scope. 
     * If the where object is not a function (Declaration Scope), then it's all the properties are
     * scanned recursively to change the declaration scope to the new one. It doesn't change the parents
     * of the objects, just the declaration scope. Usually is used, when you need wrap the object to the
     * new virtual function. 
     * @param where the object which is moved from one declaration scope to another one. 
     * @param newScope new declaration scope 
     */
    public static void changeDeclarationScope(JsObject where, DeclarationScope newScope) {
        changeDeclarationScope(where, newScope, new HashSet<String>());
    }
    
    private static void changeDeclarationScope(JsObject where, DeclarationScope newScope, Set<String> done) {
        if (!done.contains(where.getFullyQualifiedName())) {
            done.add(where.getFullyQualifiedName());
            if (where instanceof DeclarationScope) {
                if (where.isDeclared()) {
                    DeclarationScope scope = (DeclarationScope)where;
                    DeclarationScope oldScope = scope.getParentScope();
                    if (oldScope != null) {
                        oldScope.getChildrenScopes().remove(scope);
                        if (scope instanceof DeclarationScopeImpl) {
                            ((DeclarationScopeImpl)scope).setParentScope(newScope);
                        }  
                    }
                    newScope.addDeclaredScope(scope);
                }
            } else {
                for (JsObject property : where.getProperties().values()) {
                    changeDeclarationScope(property, newScope, done);
                }
            }
        }
    }
    
    /**
     * This method is useful, when you need to go through the model and be sure that
     * the algorithm will not run into endless cycle. In the model there can be references
     * to an object that can caused endless cycle. This method check whether the object has an reference or
     * the original of the reference object was already processed. Also adds the fully qualified names of the object
     * and the references to the list of processed objects. 
     * @param object object that should be processed
     * @param processedObjects list of already processed object
     * @return true if the object full qualified name or his reference full qualified name is in the processedObjects list.
     */
    public static boolean wasProcessed (JsObject object, Set<String> processedObjects) {
        if (processedObjects.contains(object.getFullyQualifiedName())) {
            return true;
        } else if (object instanceof JsReference) {
            JsObject original = ((JsReference) object).getOriginal();
            boolean isOrginalReachable = !original.isAnonymous() && !original.getName().equals(object.getName());
            JsObject origParent = original.getParent();
            while (origParent != null && isOrginalReachable) {
                if (origParent.isAnonymous() && !(origParent.getParent() != null && origParent.getParent().getParent() == null)) {
                    isOrginalReachable = false;
                }
                origParent = origParent.getParent();
            }
            if (isOrginalReachable) {
                processedObjects.add(object.getFullyQualifiedName());
                return true;
            }
            if (processedObjects.contains(original.getFullyQualifiedName())) {
                return true;
            } else {
                processedObjects.add(object.getFullyQualifiedName());
                processedObjects.add(original.getFullyQualifiedName());
            }
        } else {
            if (object.getJSKind() != JsElement.Kind.FILE) {
                processedObjects.add(object.getFullyQualifiedName());
            }
        }
        return false;
    }
    
    public static String getFQNFromType(Type type) {
        String fqn = type.getType();
        if (fqn.startsWith(SemiTypeResolverVisitor.ST_EXP)) {
            fqn = fqn.substring(SemiTypeResolverVisitor.ST_EXP.length());
        } else if (fqn.startsWith(SemiTypeResolverVisitor.ST_PRO)) {
            fqn = fqn.substring(SemiTypeResolverVisitor.ST_PRO.length());
        }
        if (fqn.contains(SemiTypeResolverVisitor.ST_PRO)) {
            fqn = fqn.replace(SemiTypeResolverVisitor.ST_PRO, ".");     //NOI18N
        }
        return fqn;
    }
}
