/*
 * 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.editor;

import com.oracle.js.parser.TokenType;
import com.oracle.js.parser.ir.ClassNode;
import com.oracle.js.parser.ir.ExportSpecifierNode;
import com.oracle.js.parser.ir.FromNode;
import com.oracle.js.parser.ir.FunctionNode;
import com.oracle.js.parser.ir.ImportSpecifierNode;
import com.oracle.js.parser.ir.LexicalContext;
import com.oracle.js.parser.ir.NameSpaceImportNode;
import com.oracle.js.parser.ir.ObjectNode;
import com.oracle.js.parser.ir.PropertyNode;
import com.oracle.js.parser.ir.UnaryNode;
import com.oracle.js.parser.ir.VarNode;
import com.oracle.js.parser.ir.visitor.NodeVisitor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.api.ColoringAttributes;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.api.SemanticAnalyzer;
import org.netbeans.modules.javascript2.doc.api.JsDocumentationSupport;
import org.netbeans.modules.javascript2.doc.spi.JsComment;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
import org.netbeans.modules.javascript2.types.api.Identifier;
import org.netbeans.modules.javascript2.model.api.JsFunction;
import org.netbeans.modules.javascript2.model.api.JsObject;
import org.netbeans.modules.javascript2.model.api.Model;
import org.netbeans.modules.javascript2.model.api.Occurrence;
import org.netbeans.modules.javascript2.types.api.Type;
import org.netbeans.modules.javascript2.model.api.ModelUtils;
import org.netbeans.modules.javascript2.editor.parser.JsParserResult;
import org.netbeans.modules.javascript2.model.api.JsReference;
import org.netbeans.modules.parsing.spi.Scheduler;
import org.netbeans.modules.parsing.spi.SchedulerEvent;

/**
 *
 * @author Petr Pisl
 */
public class JsSemanticAnalyzer extends SemanticAnalyzer<JsParserResult> {
    //public static final EnumSet<ColoringAttributes> UNUSED_VARIABLE_SET = EnumSet.of(ColoringAttributes.UNUSED, ColoringAttributes.VA);
    public static final EnumSet<ColoringAttributes> UNUSED_OBJECT_SET = EnumSet.of( ColoringAttributes.UNUSED,  ColoringAttributes.CLASS);
    public static final EnumSet<ColoringAttributes> UNUSED_METHOD_SET = EnumSet.of( ColoringAttributes.UNUSED,  ColoringAttributes.METHOD);
    public static final EnumSet<ColoringAttributes> LOCAL_VARIABLE_DECLARATION = EnumSet.of(ColoringAttributes.LOCAL_VARIABLE_DECLARATION);
    public static final EnumSet<ColoringAttributes> LOCAL_VARIABLE_DECLARATION_UNUSED = EnumSet.of(ColoringAttributes.LOCAL_VARIABLE_DECLARATION, ColoringAttributes.UNUSED);
    public static final EnumSet<ColoringAttributes> LOCAL_VARIABLE_USE = EnumSet.of(ColoringAttributes.LOCAL_VARIABLE);
    public static final EnumSet<ColoringAttributes> GLOBAL_DEFINITION = EnumSet.of(ColoringAttributes.GLOBAL, ColoringAttributes.CLASS);
    public static final EnumSet<ColoringAttributes> NUMBER_OXB_CHAR = EnumSet.of(ColoringAttributes.CUSTOM1);
    public static final EnumSet<ColoringAttributes> SEMANTIC_KEYWORD = EnumSet.of(ColoringAttributes.CUSTOM2);
    
    private boolean cancelled;
    private Map<OffsetRange, Set<ColoringAttributes>> semanticHighlights;
    private static final List<String> GLOBAL_TYPES = Arrays.asList(Type.ARRAY, Type.STRING, Type.BOOLEAN, Type.NUMBER);
    private Collection<OffsetRange> globalJsHintInlines = new ArrayList<OffsetRange>();
    
    public JsSemanticAnalyzer() {
        this.cancelled = false;
        this.semanticHighlights = null;
    }

    @Override
    public Map<OffsetRange, Set<ColoringAttributes>> getHighlights() {
        return semanticHighlights;
    }

    @Override
    public void run(JsParserResult result, SchedulerEvent event) {
        resume();

        if (isCancelled()) {
            return;
        }

        Map<OffsetRange, Set<ColoringAttributes>> highlights =
                new HashMap<OffsetRange, Set<ColoringAttributes>>(100);
        Model model = Model.getModel(result, false);
        JsObject global = model.getGlobalObject();
        Collection<Identifier> definedGlobal = ModelUtils.getDefinedGlobal(result.getSnapshot(), -1);
        for (Identifier iden: definedGlobal) {
            globalJsHintInlines.add(iden.getOffsetRange());
        }
        highlights = count(result, global, highlights, new HashSet<String>());
        highlights = processSemanticKeywords(result, highlights);
        highlights = processNumbers(result, highlights);
        
        if (highlights != null && highlights.size() > 0) {
            semanticHighlights = highlights;
        } else {
            semanticHighlights = null;
        }
    }

    private Map<OffsetRange, Set<ColoringAttributes>> count (JsParserResult result, JsObject parent, Map<OffsetRange, Set<ColoringAttributes>> highlights, Set<String> processedObjects) {
        if (ModelUtils.wasProcessed(parent, processedObjects)) {
            return highlights;
        }
        for (Iterator<? extends JsObject> it = parent.getProperties().values().iterator(); it.hasNext();) {
            JsObject object = it.next();
            if (object.getDeclarationName() != null) {
                switch (object.getJSKind()) {
                    case CONSTRUCTOR:
                    case METHOD:
                    case FUNCTION:
                    case GENERATOR:
                        if(object.isDeclared() && !object.isAnonymous() && !object.getDeclarationName().getOffsetRange().isEmpty()) {
                            EnumSet<ColoringAttributes> coloring = ColoringAttributes.METHOD_SET;
                            if (object.getModifiers().contains(Modifier.PRIVATE)) {
                                if (object.getOccurrences().isEmpty()) {
                                    coloring = UNUSED_METHOD_SET;
                                } else if (object.getOccurrences().size() == 1) {
                                    OffsetRange orDeclaration = object.getDeclarationName().getOffsetRange();
                                    OffsetRange orOccurrence = object.getOccurrences().get(0).getOffsetRange();
                                    if (orDeclaration.equals(orOccurrence)) {
                                        coloring = UNUSED_METHOD_SET;
                                    }
                                }
                            } 
                            addColoring(result, highlights, object.getDeclarationName().getOffsetRange(), coloring);
                        }
                        for(JsObject param: ((JsFunction)object).getParameters()) {
                            if (!(object instanceof JsReference && !((JsReference)object).getOriginal().isAnonymous())) {
                                count(result, param, highlights, processedObjects);
                            }
                            if (!hasSourceOccurences(result, param)) {
                                OffsetRange range = LexUtilities.getLexerOffsets(result, param.getDeclarationName().getOffsetRange());
                                if (range.getStart() < range.getEnd()) {
                                    // only for declared parameters
                                    highlights.put(range, ColoringAttributes.UNUSED_SET);
                                }
                            }
                        }
                        break;
                    case PROPERTY_GETTER:
                    case PROPERTY_SETTER:
                        int offset = LexUtilities.getLexerOffset(result, object.getDeclarationName().getOffsetRange().getStart());
                        TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(result.getSnapshot(), offset);
                        if (ts != null) {
                            ts.move(offset);
                            if (ts.moveNext() && ts.movePrevious()) {
                                Token token = LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.BLOCK_COMMENT, JsTokenId.DOC_COMMENT));
                                if (token.id() == JsTokenId.IDENTIFIER && token.length() == 3) {
                                    highlights.put(new OffsetRange(ts.offset(), ts.offset() + token.length()), ColoringAttributes.METHOD_SET);
                                }
                            }
                            highlights.put(LexUtilities.getLexerOffsets(result, object.getDeclarationName().getOffsetRange()), ColoringAttributes.FIELD_SET);
                        }
                        break;
                    case OBJECT:
                    case OBJECT_LITERAL:
                    case CLASS:    
                        if(!"UNKNOWN".equals(object.getName())) {
                             if (parent.getParent() == null && !GLOBAL_TYPES.contains(object.getName())) {
                                addColoring(result, highlights, object.getDeclarationName().getOffsetRange(), GLOBAL_DEFINITION); 
                                for (Occurrence occurence : object.getOccurrences()) {
                                    addColoring(result, highlights, occurence.getOffsetRange(), ColoringAttributes.GLOBAL_SET);
                                }
                            } else if (object.isDeclared() && !ModelUtils.PROTOTYPE.equals(object.getName()) && !object.isAnonymous()) {
                                if((object.getOccurrences().isEmpty()
                                        || (object.getOccurrences().size() == 1 && object.getOccurrences().get(0).getOffsetRange().equals(object.getDeclarationName().getOffsetRange())))
                                        && object.getModifiers().contains(Modifier.PRIVATE)) {
                                    highlights.put(LexUtilities.getLexerOffsets(result, object.getDeclarationName().getOffsetRange()), UNUSED_OBJECT_SET);
                                } else {
                                    highlights.put(LexUtilities.getLexerOffsets(result, object.getDeclarationName().getOffsetRange()), ColoringAttributes.CLASS_SET);
                                    TokenSequence<? extends JsTokenId> cts = LexUtilities.getJsTokenSequence(result.getSnapshot(), object.getDeclarationName().getOffsetRange().getStart());
                                    for (Occurrence occurrence: object.getOccurrences()) {
                                        cts.move(occurrence.getOffsetRange().getStart());
                                        if (cts.moveNext() && cts.token().id() == JsTokenId.STRING && !occurrence.getOffsetRange().equals(object.getDeclarationName().getOffsetRange())) {
                                            highlights.put(LexUtilities.getLexerOffsets(result, occurrence.getOffsetRange()), ColoringAttributes.CLASS_SET);
                                        } 
                                    }
                                }
                            }
                        }
                        break;
                    case PROPERTY:
                    case FIELD:
                        if(object.isDeclared()) {
                            addColoring(result, highlights, object.getDeclarationName().getOffsetRange(), ColoringAttributes.FIELD_SET);
                            for(Occurrence occurence: object.getOccurrences()) {
                                addColoring(result, highlights, occurence.getOffsetRange(), ColoringAttributes.FIELD_SET);
                            }
                        } else {
                            // we need to check whether the fiels is not used in aa["bb"], then bb color with black 
                            TokenSequence<? extends JsTokenId> cts = LexUtilities.getJsTokenSequence(result.getSnapshot(), object.getOffset());
                            cts.move(object.getOffsetRange().getStart());
                            if (cts.moveNext() && cts.token().id() == JsTokenId.STRING) {
                                addColoring(result, highlights, object.getOffsetRange(), ColoringAttributes.FIELD_SET);
                            }
                            for (Occurrence occurrence : object.getOccurrences()) {
                                cts.move(occurrence.getOffsetRange().getStart());
                                if (cts.moveNext() && cts.token().id() == JsTokenId.STRING) {
                                    addColoring(result, highlights, occurrence.getOffsetRange(), ColoringAttributes.FIELD_SET);
                                }
                                
                            }
                        }
                        break;
                    case VARIABLE:
                        if (parent.getParent() == null && !GLOBAL_TYPES.contains(object.getName())) {
                            addColoring(result, highlights, object.getDeclarationName().getOffsetRange(), ColoringAttributes.GLOBAL_SET);
                            for(Occurrence occurence: object.getOccurrences()) {
                                addColoring(result, highlights, occurence.getOffsetRange(), ColoringAttributes.GLOBAL_SET);
                            }
                        } else {
                            if ((object.getOccurrences().isEmpty()
                                    || (object.getOccurrences().size() == 1 && object.getOccurrences().get(0).getOffsetRange().equals(object.getDeclarationName().getOffsetRange())))
                                    && !GLOBAL_TYPES.contains(object.getName())) {
                                OffsetRange range = object.getDeclarationName().getOffsetRange();
                                if (range.getStart() < range.getEnd()) {
                                    // some virtual variables (like arguments) doesn't have to be declared, but are in the model
                                    if (object.getModifiers().contains(Modifier.PRIVATE) || object.getModifiers().contains(Modifier.PROTECTED)) { 
                                        highlights.put(LexUtilities.getLexerOffsets(result, object.getDeclarationName().getOffsetRange()), LOCAL_VARIABLE_DECLARATION_UNUSED);
                                    } else {
                                        highlights.put(LexUtilities.getLexerOffsets(result, object.getDeclarationName().getOffsetRange()), ColoringAttributes.UNUSED_SET);
                                    }
                                }
                            } else if (object instanceof JsObject && !ModelUtils.ARGUMENTS.equals(object.getName())) {   // NOI18N
                                if (object.getOccurrences().size() <= ((JsObject)object).getAssignmentCount()) {
                                    // probably is used only on the left site => is unused
                                    if (object.getDeclarationName().getOffsetRange().getLength() > 0) {
                                        highlights.put(LexUtilities.getLexerOffsets(result, object.getDeclarationName().getOffsetRange()), ColoringAttributes.UNUSED_SET);
                                    }
                                    for(Occurrence occurence: object.getOccurrences()) {
                                        if (occurence.getOffsetRange().getLength() > 0) {
                                            highlights.put(LexUtilities.getLexerOffsets(result, occurence.getOffsetRange()), ColoringAttributes.UNUSED_SET);
                                        }
                                    }
                                } else if (object.getModifiers().contains(Modifier.PRIVATE) || object.getModifiers().contains(Modifier.PROTECTED)) {
                                    OffsetRange decOffset = object.getDeclarationName().getOffsetRange();
                                    addColoring(result, highlights, decOffset, LOCAL_VARIABLE_DECLARATION);
                                    for(Occurrence occurence: object.getOccurrences()) {
                                        if (occurence.getOffsetRange().getLength() > 0 && !occurence.getOffsetRange().equals(decOffset)) {
                                            addColoring(result, highlights, occurence.getOffsetRange(), LOCAL_VARIABLE_USE);
                                        }
                                    }
                                }
                            }
                        }
                }
            }
            if (isCancelled()) {
                highlights = null;
                break;
            }
            if (!(object instanceof JsReference && ModelUtils.isDescendant(object, ((JsReference)object).getOriginal()))) {
                highlights = count(result, object, highlights, processedObjects);
            }
        }

        return highlights;
    }

    private Map<OffsetRange, Set<ColoringAttributes>> processSemanticKeywords(final JsParserResult result,
            final Map<OffsetRange, Set<ColoringAttributes>> highlights) {

        FunctionNode root = result.getRoot();
        if (root == null) {
            return highlights;
        }

        NodeVisitor visitor = new NodeVisitor(new LexicalContext()) {

            @Override
            public boolean enterFunctionNode(FunctionNode functionNode) {
                if (functionNode.isModule()) {
                    functionNode.visitImports(this);
                    functionNode.visitExports(this);
                }

                if (functionNode.isAsync() && !functionNode.isMethod()) {
                    int pos = com.oracle.js.parser.Token.descPosition(functionNode.getFirstToken());
                    if (functionNode.getKind() != FunctionNode.Kind.ARROW) {
                        // in arrow function async is the first token
                        pos--;
                    }
                    TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsPositionedSequence(result.getSnapshot(), pos);
                    if (ts != null) {
                        Token<? extends JsTokenId> token = LexUtilities.findPreviousNonWsNonComment(ts);
                        if (token != null && token.id() == JsTokenId.IDENTIFIER && "async".equals(token.text().toString())) {
                            highlights.put(LexUtilities.getLexerOffsets(result,
                                    new OffsetRange(ts.offset(), ts.offset() + token.length())), SEMANTIC_KEYWORD);
                        }
                    }
                }
                return super.enterFunctionNode(functionNode);
            }

            @Override
            public boolean enterImportSpecifierNode(ImportSpecifierNode importSpecifierNode) {
                if (importSpecifierNode.getIdentifier() != null) {
                    int start = importSpecifierNode.getIdentifier().getFinish();
                    TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsPositionedSequence(result.getSnapshot(), start);
                    if (ts != null) {
                        Token<? extends JsTokenId> token = LexUtilities.findNextNonWsNonComment(ts);
                        if (token != null && token.id() == JsTokenId.IDENTIFIER && ts.offset() < importSpecifierNode.getBindingIdentifier().getStart()) {
                            // it has to be "as"
                            highlights.put(LexUtilities.getLexerOffsets(result,
                                    new OffsetRange(ts.offset(), ts.offset() + token.length())), SEMANTIC_KEYWORD);
                        }
                    }
                }
                return false;
            }

            @Override
            public boolean enterExportSpecifierNode(ExportSpecifierNode exportSpecifierNode) {
                if (exportSpecifierNode.getExportIdentifier() != null) {
                    int start = exportSpecifierNode.getIdentifier().getFinish();
                    TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsPositionedSequence(result.getSnapshot(), start);
                    if (ts != null) {
                        Token<? extends JsTokenId> token = LexUtilities.findNextNonWsNonComment(ts);
                        if (token != null && token.id() == JsTokenId.IDENTIFIER && ts.offset() < exportSpecifierNode.getExportIdentifier().getStart()) {
                            // it has to be "as"
                            highlights.put(LexUtilities.getLexerOffsets(result,
                                    new OffsetRange(ts.offset(), ts.offset() + token.length())), SEMANTIC_KEYWORD);
                        }
                    }
                }
                return false;
            }

            @Override
            public boolean enterNameSpaceImportNode(NameSpaceImportNode nameSpaceImportNode) {
                int start = nameSpaceImportNode.getBindingIdentifier().getStart();
                if (start <= 0) {
                    return false;
                }

                TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsPositionedSequence(result.getSnapshot(), start - 1);
                if (ts != null) {
                    Token<? extends JsTokenId> token = LexUtilities.findPreviousNonWsNonComment(ts);
                    if (token != null && token.id() == JsTokenId.IDENTIFIER && ts.token().length() > 1) {
                        // it has to be "as"
                        highlights.put(LexUtilities.getLexerOffsets(result,
                                new OffsetRange(ts.offset(), ts.offset() + token.length())), SEMANTIC_KEYWORD);
                    }
                }
                return false;
            }

            @Override
            public boolean enterFromNode(FromNode fromNode) {
                highlights.put(LexUtilities.getLexerOffsets(result,
                                new OffsetRange(fromNode.getStart(), fromNode.getStart() + "from".length())), SEMANTIC_KEYWORD); // NOI18N
                return false;
            }

            @Override
            public boolean enterClassNode(ClassNode classNode) {
                for (PropertyNode p : classNode.getClassElements()) {
                    handleProperty(p, true);
                }
                return super.enterClassNode(classNode);
            }

            @Override
            public boolean enterObjectNode(ObjectNode objectNode) {
                for (PropertyNode p : objectNode.getElements()) {
                    handleProperty(p, false);
                }
                return super.enterObjectNode(objectNode);
            }

            @Override
            public boolean enterVarNode(VarNode varNode) {
                if (varNode.isLet()) {
                    TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsPositionedSequence(result.getSnapshot(), varNode.getStart() - 1);
                    if (ts != null) {
                        Token<? extends JsTokenId> token = LexUtilities.findPreviousNonWsNonComment(ts);
                        if (token != null && token.id() == JsTokenId.RESERVED_LET) {
                            highlights.put(LexUtilities.getLexerOffsets(result,
                                    new OffsetRange(ts.offset(), ts.offset() + token.length())), SEMANTIC_KEYWORD);
                        }
                    }
                }
                return super.enterVarNode(varNode);
            }

            @Override
            public boolean enterUnaryNode(UnaryNode unaryNode) {
                if (unaryNode.isTokenType(TokenType.AWAIT)) {
                    TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsPositionedSequence(result.getSnapshot(), unaryNode.getStart());
                    if (ts != null) {
                        Token<? extends JsTokenId> token = LexUtilities.findPreviousNonWsNonComment(ts);
                        if (token != null && token.id() == JsTokenId.RESERVED_AWAIT) {
                            highlights.put(LexUtilities.getLexerOffsets(result,
                                    new OffsetRange(ts.offset(), ts.offset() + token.length())), SEMANTIC_KEYWORD);
                        }
                    }
                }
                return super.enterUnaryNode(unaryNode);
            }

            private void handleProperty(PropertyNode p, boolean classElement) {
                int offset = -1;
                if ((p.getValue() instanceof FunctionNode) && ((FunctionNode) p.getValue()).isAsync()) {
                    TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsPositionedSequence(result.getSnapshot(), p.getStart() - 1);
                    if (ts != null) {
                        Token<? extends JsTokenId> token = LexUtilities.findPreviousNonWsNonComment(ts);
                        if (token != null && token.id() == JsTokenId.IDENTIFIER && "async".equals(token.text().toString())) {
                            offset = ts.offset();
                            highlights.put(LexUtilities.getLexerOffsets(result,
                                    new OffsetRange(ts.offset(), ts.offset() + token.length())), SEMANTIC_KEYWORD);
                        }
                    }
                }
                if (classElement && p.isStatic()) {
                    TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsPositionedSequence(result.getSnapshot(), offset >= 0 ? offset - 1 : p.getStart() - 1);
                    if (ts != null) {
                        Token<? extends JsTokenId> token = LexUtilities.findPreviousNonWsNonComment(ts);
                        if (token != null && token.id() == JsTokenId.RESERVED_STATIC) {
                            highlights.put(LexUtilities.getLexerOffsets(result,
                                    new OffsetRange(ts.offset(), ts.offset() + token.length())), SEMANTIC_KEYWORD);
                        }
                    }
                }
            }
        };

        root.accept(visitor);
        return highlights;
    }
    
    private Map<OffsetRange, Set<ColoringAttributes>> processNumbers(JsParserResult result, Map<OffsetRange, Set<ColoringAttributes>> highlights) {
        TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(result.getSnapshot(), 0);
        if (ts != null) {
            ts.move(0);
            
            List<JsTokenId> lookFor = new ArrayList<JsTokenId>(3);
            lookFor.add(JsTokenId.NUMBER);
            Token<? extends JsTokenId> token;
            while (ts.moveNext() && (token = LexUtilities.findNextToken(ts, lookFor)) != null) {
                if (token.id() == JsTokenId.NUMBER) {
                    String number = token.text().toString().toLowerCase(Locale.ENGLISH);
                    if (number.startsWith("0b") || number.startsWith("0x") || number.startsWith("0o")) { //NOI18N
                        highlights.put(LexUtilities.getLexerOffsets(result, new OffsetRange(ts.offset() + 1, ts.offset() + 2)), NUMBER_OXB_CHAR);
                    }
                }
            }
        }
        return highlights;
    }
    
    private void addColoring(JsParserResult result, Map<OffsetRange, Set<ColoringAttributes>> highlights, OffsetRange astRange, Set<ColoringAttributes> coloring) {
        int start = result.getSnapshot().getOriginalOffset(astRange.getStart());
        int end = result.getSnapshot().getOriginalOffset(astRange.getEnd());
        if (start > -1 && end > -1 && start < end && !isInComment(result, astRange)) {
            OffsetRange range = start == astRange.getStart() ? astRange : new OffsetRange(start, end);
            highlights.put(range, coloring);
        }
    }
    
    @Override
    public int getPriority() {
        return 0;
    }

    @Override
    public Class<? extends Scheduler> getSchedulerClass() {
        return Scheduler.EDITOR_SENSITIVE_TASK_SCHEDULER;
    }

    @Override
    public synchronized void cancel() {
        cancelled = true;
    }

    protected final synchronized boolean isCancelled() {
        return cancelled;
    }

    protected final synchronized void resume() {
        cancelled = false;
    }

    private boolean hasSourceOccurences(JsParserResult result, JsObject param) {
        if (param.getOccurrences().isEmpty()) {
            return false;
        }
        if (param.getOccurrences().size() == 1 && param.getOccurrences().get(0).getOffsetRange().equals(param.getDeclarationName().getOffsetRange())) {
            return false;
        }

        int sourceOccurenceCount = 0;
        for (Occurrence occurrence : param.getOccurrences()) {
            if (!isInComment(result, occurrence.getOffsetRange())) {
                 sourceOccurenceCount++;
            }
            if (sourceOccurenceCount > 1) {
                return true;
            }
        }
        return false;
    }

    private boolean isInComment(JsParserResult result, OffsetRange range) {
        for (JsComment comment : JsDocumentationSupport.getDocumentationHolder(result).getCommentBlocks().values()) {
            if (comment.getOffsetRange().containsInclusive(range.getStart())) {
                return true;
            }
        }
        if (globalJsHintInlines.contains(range)) {
            return true;
        }
        return false;
    }

}
