/*
 * 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 org.netbeans.modules.javascript2.model.api.ModelUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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.model.api.JsElement;
import org.netbeans.modules.javascript2.model.api.JsFunction;
import org.netbeans.modules.javascript2.model.api.JsObject;
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.openide.filesystems.FileObject;

/**
 *
 * @author Petr Pisl
 */
public class JsFunctionImpl extends DeclarationScopeImpl implements JsFunction {

    final private HashMap <String, JsObject> parametersByName;
    final private List<JsObject> parameters;
    final private Set<TypeUsage> returnTypes;
    private boolean isStrict;

    public JsFunctionImpl(DeclarationScope scope, JsObject parentObject, Identifier name,
            List<Identifier> parameters, OffsetRange offsetRange, String mimeType, String sourceLabel) {
        super(scope, parentObject, name, offsetRange, mimeType, sourceLabel);
        this.parametersByName = new HashMap<String, JsObject>(parameters.size());
        this.parameters = new ArrayList<JsObject>(parameters.size());
        for (Identifier identifier : parameters) {
            JsObject parameter = new ParameterObject(this, identifier, mimeType, sourceLabel);
            addParameter(parameter);
        }
        setAnonymous(false);
        this.returnTypes = new HashSet<TypeUsage>();
        setDeclared(true);
        if (parentObject != null) {
            // creating arguments variable
            JsObjectImpl arguments = new JsObjectImpl(this, 
                    new Identifier(ModelUtils.ARGUMENTS, new OffsetRange(name.getOffsetRange().getStart(), name.getOffsetRange().getStart())), 
                    name.getOffsetRange(),  false, EnumSet.of(Modifier.PRIVATE), mimeType, sourceLabel);
            arguments.addAssignment(new TypeUsage("Arguments", getOffset(), true), getOffset());    // NOI18N
            this.addProperty(arguments.getName(), arguments);
        }
    }

    protected JsFunctionImpl(FileObject file, JsObject parentObject, Identifier name,
            List<Identifier> parameters, String mimeType, String sourceLabel) {
        this(null, parentObject, name, parameters, name.getOffsetRange(), mimeType, sourceLabel);
        this.setFileObject(file);
        this.setDeclared(false);
    }

    private JsFunctionImpl(FileObject file, Identifier name, String mimeType, String sourceLabel) {
        this(null, null, name, Collections.EMPTY_LIST, name.getOffsetRange(), mimeType, sourceLabel);
        this.setFileObject(file);
    }
    
    public static JsFunctionImpl createGlobal(FileObject fileObject, int length, String mimeType) {
        String name = fileObject != null ? fileObject.getName() : "VirtualSource"; //NOI18N
        Identifier ident = new Identifier(name, new OffsetRange(0, length));
        return new JsFunctionImpl(fileObject, ident, mimeType, null);
    }
    
    @Override
    public final Collection<? extends JsObject> getParameters() {
        return this.parameters;
    }

    public final void addParameter(JsObject object) {
        assert object.getParent() == this;
        this.parametersByName.put(object.getName(), object);
        this.parameters.add(object);
    }

    @Override
    public Kind getJSKind() {
        if (kind != null) {
            return kind;
        }
        if (getParent() == null) {
            // global function
            return JsElement.Kind.FILE;
        }
        String name = getName();
        if (name != null && name.startsWith("get ")) { //NOI18N
            return JsElement.Kind.PROPERTY_GETTER;
        }
        if (name != null && name.startsWith("set ")) { //NOI18N
            return JsElement.Kind.PROPERTY_SETTER;
        }
        if (getParent() != null /*&& getParent() instanceof JsFunction*/) {
             JsObject prototype = null;
            for (JsObject property : getProperties().values()) {
                if (property.isDeclared() 
                        && (property.getModifiers().contains(Modifier.PROTECTED)
                        || (property.getModifiers().contains(Modifier.PUBLIC) &&  !property.getModifiers().contains(Modifier.STATIC)))
                        && !isAnonymous() && !property.isAnonymous()
                        && (property.getDeclarationName() != null && property.getDeclarationName().getOffsetRange().getStart() < property.getDeclarationName().getOffsetRange().getEnd())) {
                    if(!ModelUtils.PROTOTYPE.equals(getParent().getName())) {
                        return JsElement.Kind.CONSTRUCTOR;
                    }
                }
                if (ModelUtils.PROTOTYPE.equals(property.getName())) {
                    prototype = property;
                }
            }
            if (prototype != null /*&& !prototype.getProperties().isEmpty()*/) {
                return JsElement.Kind.CONSTRUCTOR;
            }
        }
//        if (getParent() != null && !getParent().isDeclared()) {
//            
//        }

        JsElement.Kind result = JsElement.Kind.FUNCTION;

        if (!(getParent() instanceof JsObjectReference) && getParent().getJSKind() != JsElement.Kind.FILE) {
            result = JsElement.Kind.METHOD;
        }
        return result;
    }

    @Override
    public JsObject getParameter(String name) {
        JsObject result = parametersByName.get(name);
        return result;
    }

    private boolean areReturnTypesResolved = false;
    
    @Override
    public Collection<? extends TypeUsage> getReturnTypes() {
        if (areReturnTypesResolved) {
            return Collections.EMPTY_LIST;
        }
        Collection<TypeUsage> returns = new HashSet();
        HashSet<String> nameReturnTypes = new HashSet<String>();
        areReturnTypesResolved = true;
        for(TypeUsage type : returnTypes) {
             if (type.isResolved()) {
                 if (!nameReturnTypes.contains(type.getType())){
                    returns.add(type);
                    nameReturnTypes.add(type.getType());
                 }
            } else {
                 if (type.getType().startsWith("@")) {
                     String typeName = type.getType();
                     if (!(typeName.endsWith(getName()) && typeName.startsWith("@call"))) {
                        Collection<TypeUsage> resolved = ModelUtils.resolveTypeFromSemiType(this, type);
                        for (TypeUsage typeResolved : resolved) {
                            if (!nameReturnTypes.contains(type.getType())) {
                                returns.add(typeResolved);
                                nameReturnTypes.add(typeResolved.getType());
                            }
                        }
                     }
                 } else {
                    JsObject jsObject = ModelUtils.getJsObjectByName(this,type.getType());
                    if (jsObject == null) {
                        // try to find according the fqn
                        JsObject global = ModelUtils.getGlobalObject(this);
                        jsObject = ModelUtils.findJsObjectByName(global, type.getType());
                    }
                    if(jsObject != null) {
                       Collection<TypeUsage> resolveAssignments = resolveAssignments(jsObject, type.getOffset());
                       for (TypeUsage typeResolved: resolveAssignments) {
                           if (!nameReturnTypes.contains(typeResolved.getType())){
                              returns.add(typeResolved);
                              nameReturnTypes.add(typeResolved.getType());
                           }
                       }
                    } else {
                        returns.add(type);
                        nameReturnTypes.add(type.getType());
                    }
                 }
            }
        }
        areReturnTypesResolved = false;
        return returns;
    }    
        
    @Override
    public void addReturnType(TypeUsage type) {
        boolean isThere = false;
        for (TypeUsage typeUsage : this.returnTypes) {
            if (type.getType().equals(typeUsage.getType())) {
                isThere = true;
            }
        }
        if (!isThere){
            this.returnTypes.add(type);
        }
    }
    
    public void addReturnType(Collection<TypeUsage> types) {
        for (TypeUsage typeUsage : types) {
            addReturnType(typeUsage);
        }
    }
    
    public boolean areReturnTypesEmpty() {
        return returnTypes.isEmpty();
    }

    @Override
    public boolean moveProperty(String name, JsObject newParent) {
        JsObject property = getProperty(name);
        if (property != null && (newParent instanceof DeclarationScope)) {
            ModelUtils.changeDeclarationScope(property, (DeclarationScope)newParent);
        }
        return super.moveProperty(name, newParent); 
    }

    @Override
    public void resolveTypes(JsDocumentationHolder docHolder) {
        super.resolveTypes(docHolder);
        if (!(returnTypes.size() == 1 && Type.UNDEFINED.equals(returnTypes.iterator().next().getType()))) {
            HashSet<String> nameReturnTypes = new HashSet<String>();
            Collection<TypeUsage> resolved = new ArrayList();
            for (TypeUsage type : returnTypes) {
                if (!(type.getType().equals(Type.UNRESOLVED) && returnTypes.size() > 1)) {
                    if (!type.isResolved()) {
                        for (TypeUsage rType : ModelUtils.resolveTypeFromSemiType(this, type)) {
                            if (!nameReturnTypes.contains(rType.getType())) {
                                if ("@this;".equals(type.getType())) { // NOI18N
                                    rType = new TypeUsage(rType.getType(), -1, rType.isResolved());
                                }
                                resolved.add(rType);
                                nameReturnTypes.add(rType.getType());
                            }
                        }
    //                    resolved.addAll(ModelUtils.resolveTypeFromSemiType(this, type));
                    } else {
                        if (!nameReturnTypes.contains(type.getType())) {
                            resolved.add(type);
                            nameReturnTypes.add(type.getType());
                        }
                    }
                }
            }

            for (TypeUsage type : resolved) {
                if (type.getOffset() > 0) {
                    String typeName = type.getType();
                    JsObject jsObject = null;
                    // at first check whether is not a parameter
                    if (typeName.indexOf('.') == -1) {
                        JsObject parameter = null;
                        DeclarationScope scope = this;
                        while (scope != null && parameter == null && jsObject == null) {
                            if (scope instanceof JsFunction) {
                                parameter = ((JsFunction) scope).getParameter(typeName);
                            }
                            jsObject = ((JsObject) scope).getProperty(typeName);
                            scope = scope.getParentScope();
                        }
                        if (jsObject == null && parameter != null) {
                            jsObject = parameter;
                        }
                        if (jsObject != null) {
                            jsObject.addOccurrence(new OffsetRange(type.getOffset(), type.getOffset() + typeName.length()));
                        }
                    }
                    if (jsObject == null) {
                        jsObject = ModelUtils.findJsObjectByName(this, typeName);
                        if (jsObject == null) {
                            JsObject global = ModelUtils.getGlobalObject(this);
                            jsObject = ModelUtils.findJsObjectByName(global, typeName);
                        }
                        if (jsObject != null && containsOffset(type.getOffset()) && !getJSKind().equals(JsElement.Kind.FILE)) {
                            int index = typeName.lastIndexOf('.');
                            int typeLength = (index > -1) ? typeName.length() - index - 1 : typeName.length();
                            ((JsObjectImpl)jsObject).addOccurrence(new OffsetRange(type.getOffset(), jsObject.isAnonymous() ? type.getOffset() : type.getOffset() + typeLength));
                        }
                    }
                }
            }
            returnTypes.clear();
            returnTypes.addAll(resolved);
        } else if (getJSKind() == JsElement.Kind.CONSTRUCTOR) {
            Collection<TypeUsage> resolved = ModelUtils.resolveTypeFromSemiType(this, returnTypes.iterator().next());
            returnTypes.clear();
            returnTypes.addAll(resolved);
        } else if (returnTypes.size() == 1) {
            TypeUsage type = returnTypes.iterator().next();
            if (Type.UNDEFINED.equals(type.getType()) && !type.isResolved()) {
                returnTypes.clear();
                returnTypes.add(new TypeUsage(type.getType(), type.getOffset(), true));
            }
        }
         
        // parameters and type type resolving for occurrences
        JsObject global = ModelUtils.getGlobalObject(this);
        for(JsObject param : parameters) {
            Collection<? extends TypeUsage> types = param.getAssignmentForOffset(param.getDeclarationName().getOffsetRange().getStart());
            for(TypeUsage type: types) {
                JsObject jsObject = ModelUtils.findJsObjectByName(global, type.getType());//getJsObjectByName(this, type.getType());
                if (jsObject != null) {
                    ModelUtils.addDocTypesOccurence(jsObject, docHolder);
                    moveOccurrenceOfProperties((JsObjectImpl)jsObject, param);
                    if (type.getType().indexOf('.') > -1) {
                        // mark occurrences also for the parent if the type is like Contex.Object
                        String[] typeParts = type.getType().split("\\.");
                        JsObject parent = jsObject.getParent();
                        for (int i = (typeParts.length - 2); i > -1 && parent != null; i--) {
                            if (parent.getName().equals(typeParts[i])) {
                                ModelUtils.addDocTypesOccurence(parent, docHolder);
                            }
                            parent = parent.getParent();
                        }
                    }
                }
            }
            List<JsObject> paramProperties = new ArrayList(param.getProperties().values());
            for(JsObject paramProperty: paramProperties) {
               ((JsObjectImpl)paramProperty).resolveTypes(docHolder);
            }
        }
    }

//    @Override
//    public String toString() {
//        return "JsFunctionImpl{" + "declarationName=" + getDeclarationName() + ", parent=" + getParent() + ", kind=" + kind + ", parameters=" + parameters + ", returnTypes=" + returnTypes + '}';
//    }

    @Override
    protected void correctTypes(String fromType, String toType) {
        super.correctTypes(fromType, toType);
        String typeR;
        String typeFQN;
        Set<TypeUsage> copy = new HashSet<TypeUsage>(returnTypes);
        for (TypeUsage type : copy) {
            typeFQN = type.getType();
            typeR = replaceTypeInFQN(typeFQN, fromType, toType);
            if (typeR != null) {
                returnTypes.remove(type);
                returnTypes.add(new TypeUsage(typeR, type.getOffset(), type.isResolved()));
            }
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getDeclarationName().getName()).append("()");
        return sb.toString();
    }

    public boolean isStrict() {
        return isStrict;
    }

    public void setStrict(boolean isStrict) {
        this.isStrict = isStrict;
    }
    
}
