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

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.javascript2.doc.api.JsDocumentationSupport;
import org.netbeans.modules.javascript2.doc.spi.DocParameter;
import org.netbeans.modules.javascript2.doc.spi.JsComment;
import org.netbeans.modules.javascript2.doc.spi.JsDocumentationHolder;
import org.netbeans.modules.javascript2.types.api.DeclarationScope;
import org.netbeans.modules.javascript2.types.api.Identifier;
import org.netbeans.modules.javascript2.model.api.JsElement;
import org.netbeans.modules.javascript2.model.api.JsFunction;
import org.netbeans.modules.javascript2.model.api.JsObject;
import org.netbeans.modules.javascript2.model.api.JsWith;
import org.netbeans.modules.javascript2.model.api.ModelUtils;
import org.netbeans.modules.javascript2.types.spi.ParserResult;
import org.openide.filesystems.FileObject;

/**
 *
 * @author Petr Pisl
 */
public class OccurrenceBuilder {
    private static class Item {
        final DeclarationScope scope;
        final JsObject currentParent;
        final JsWith currentWith;
        final boolean isFunction;
        final boolean leftSite;
        final OffsetRange range;

        public Item(OffsetRange range, DeclarationScope scope, JsObject currentParent, JsWith currentWith, boolean isFunction, boolean leftSite) {
            this.scope = scope;
            this.currentParent = currentParent;
            this.isFunction = isFunction;
            this.leftSite = leftSite;
            this.range = range;
            this.currentWith = currentWith;
        }
    }
    private final Map<String, Map<OffsetRange, Item>> holder;
    private final ParserResult parserResult;
    
    public OccurrenceBuilder(ParserResult parserResult) {
        holder = new HashMap<String, Map<OffsetRange, Item>>();
        this.parserResult = parserResult;
    }
    
    public void addOccurrence(String name, OffsetRange range, DeclarationScope whereUsed, JsObject currentParent, JsWith inWith, boolean isFunction, boolean leftSite) {
        Map<OffsetRange, Item> items = holder.get(name);
        if (items == null) {
            items = new HashMap<OffsetRange, Item>(1);
            holder.put(name, items);
        }
        if (!items.containsKey(range)) {
            items.put(range, new Item(range, whereUsed, currentParent, inWith, isFunction, leftSite));
        }
    }
    
    public void processOccurrences(JsObject global) {
        for (String name : holder.keySet()) {
            Map<OffsetRange, Item> items = holder.get(name);
            for (Item item : items.values()) {
                processOccurrence(global, name, item);
            }
        }
        holder.clear(); // we don't need to keep it anymore.
        Collection<Identifier> usedInJsHintInline = ModelUtils.getDefinedGlobal(parserResult.getSnapshot(), global.getOffset());
        for (Identifier iden: usedInJsHintInline) {
            JsObject object = global.getProperty(iden.getName());
            if (object != null) {
                object.addOccurrence(iden.getOffsetRange());
            }
        }
    }

    private void processOccurrence(JsObject global, String name, Item item) {
        JsObject property = null;
        JsObject parameter = null;
        DeclarationScope scope = item.scope;
        JsObject parent = item.currentParent;
        if (!(parent instanceof JsWith || (parent.getParent() != null && parent.getParent() instanceof JsWith))) {
            while (scope != null && property == null && parameter == null) {
                if (scope instanceof JsFunction) {
                    parameter = ((JsFunction) scope).getParameter(name);
                }
                property = ((JsObject) scope).getProperty(name);
                scope = scope.getParentScope();
            }
            if(parameter != null) {
                if (property == null) {
                    property = parameter;
                } else {
                    if(property.getJSKind() != JsElement.Kind.VARIABLE) {
                        property = parameter;
                    }
                }
            }
        } else {
            if (!(parent instanceof JsWith) && (parent.getParent() != null && parent.getParent() instanceof JsWith)) {
                parent = parent.getParent();
            }
            property = parent.getProperty(name);
        }

        if (!(parent instanceof JsWith) && property == null) {
            JsObject possibleParent = parent;
            
            while (property == null && possibleParent != null) {
                property = possibleParent.getProperty(name);
                possibleParent = possibleParent.getParent();
                if (possibleParent != null && possibleParent.equals(possibleParent.getParent())) {
                    break;
                }
            }
        }
        
        if (property != null) {

            // occurence in the doc
            addDocNameOccurence(((JsObjectImpl)property));
            addDocTypesOccurence(((JsObjectImpl)property));

            ((JsObjectImpl)property).addOccurrence(item.range);
        } else {
            // it's a new global variable?
            Identifier nameIden = ModelElementFactory.create(parserResult, name, item.range.getStart(), item.range.getEnd());
            if (nameIden != null) {
                if (item.currentWith != null) {
                    JsObject with = (JsObject)item.currentWith;
                    property = with.getProperty(name);
                    if (property != null) {
                        ((JsObjectImpl)property).addOccurrence(item.range);
                    } else {
                        createNewProperty(with, item, nameIden);
                    }
                } else {
                    if (!(parent instanceof JsWith)) {
                            parent = global;
                    }
                    createNewProperty(parent, item, nameIden);
                }
            }
        }

    }
    
    private void createNewProperty(JsObject parent, Item item, Identifier nameIden) {
        JsObjectImpl newObject;
        if (!item.isFunction) {
            newObject = new JsObjectImpl(parent, nameIden, nameIden.getOffsetRange(),
                    item.leftSite, parserResult.getSnapshot().getMimeType(), null);
        } else {
            FileObject fo = parserResult.getSnapshot().getSource().getFileObject();
            newObject = new JsFunctionImpl(fo, parent, nameIden, Collections.EMPTY_LIST,
                    parserResult.getSnapshot().getMimeType(), null);
        }
        newObject.addOccurrence(nameIden.getOffsetRange());
        parent.addProperty(nameIden.getName(), newObject);
        addDocNameOccurence(newObject);
        addDocTypesOccurence(newObject);
    }
    
    private void addDocNameOccurence(JsObjectImpl jsObject) {
        JsDocumentationHolder holder = JsDocumentationSupport.getDocumentationHolder(parserResult);
        JsComment comment = holder.getCommentForOffset(jsObject.getOffset(), holder.getCommentBlocks());
        if (comment != null) {
            for (DocParameter docParameter : comment.getParameters()) {
                Identifier paramName = docParameter.getParamName();
                String name = (docParameter.getParamName() == null) ? "" : docParameter.getParamName().getName(); //NOI18N
                if (name.equals(jsObject.getName())) {
                    jsObject.addOccurrence(paramName.getOffsetRange());
                }
            }
        }
    }

    private void addDocTypesOccurence(JsObjectImpl jsObject) {
        JsDocumentationHolder holder = JsDocumentationSupport.getDocumentationHolder(parserResult);
        if (holder.getOccurencesMap().containsKey(jsObject.getName())) {
            for (OffsetRange offsetRange : holder.getOccurencesMap().get(jsObject.getName())) {
                ((JsObjectImpl)jsObject).addOccurrence(offsetRange);
            }
        }
    }
}
