/*
 *
 *  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.apache.royale.compiler.internal.codegen.typedefs.reference;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

import org.apache.royale.compiler.clients.ExternCConfiguration;
import org.apache.royale.compiler.clients.ExternCConfiguration.ExcludedMember;
import org.apache.royale.compiler.clients.problems.ProblemQuery;
import org.apache.royale.compiler.internal.codegen.typedefs.utils.DebugLogUtils;

import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.NodeUtil;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.JSType;

public class ReferenceModel
{
    public ProblemQuery problems;
    private ExternCConfiguration configuration;
    private Compiler jscompiler;

    private List<String> namespaces = new ArrayList<String>();

    private HashMap<String, ClassReference> typedefs = new HashMap<String, ClassReference>();
    private HashMap<String, ClassReference> classes = new HashMap<String, ClassReference>();
    private HashMap<String, FunctionReference> functions = new HashMap<String, FunctionReference>();
    private HashMap<String, ConstantReference> constants = new HashMap<String, ConstantReference>();

    //    public Compiler getJSCompiler()
    //    {
    //        return jscompiler;
    //    }

    public void setJSCompiler(Compiler compiler)
    {
        this.jscompiler = compiler;
    }

    public ExternCConfiguration getConfiguration()
    {
        return configuration;
    }

    public ClassReference getObjectReference()
    {
        return classes.get("Object");
    }

    public Collection<String> getNamespaces()
    {
        return namespaces;
    }

    public Collection<ClassReference> getTypedefs()
    {
        return typedefs.values();
    }

    public Collection<ClassReference> getClasses()
    {
        return classes.values();
    }

    public Collection<FunctionReference> getFunctions()
    {
        return functions.values();
    }

    public Collection<ConstantReference> getConstants()
    {
        return constants.values();
    }

    public ReferenceModel(ExternCConfiguration config)
    {
        this.configuration = config;
    }

    public ClassReference getClassReference(String qualifiedName)
    {
        return classes.get(qualifiedName);
    }

    public ClassReference getInterfaceReference(String qualifiedName)
    {
        ClassReference reference = classes.get(qualifiedName);
        if (reference != null && reference.isInterface())
            return reference;
        return null;
    }

    public ClassReference getTypeDefReference(String qualifiedName)
    {
        return typedefs.get(qualifiedName);
    }

    public void addNamespace(Node node, String qualifiedName)
    {
        if (namespaces.contains(qualifiedName))
        {
            err("Duplicate namesapce [" + qualifiedName + "]");
            return;
        }

        log("Model.addNamespace(" + qualifiedName + ")");

        namespaces.add(qualifiedName);
    }

    public void addClass(Node node, String qualifiedName)
    {
        if (getConfiguration().isClassToFunctions(qualifiedName))
        {
            addFunction(node, qualifiedName);
            return;
        }

        if (classes.containsKey(qualifiedName))
        {
            err("Duplicate class [" + qualifiedName + "]");
            return;
        }

        log("Model.addClass(" + qualifiedName + ")");
        ClassReference reference = new ClassReference(this, node, qualifiedName);

        // TODO (mschmalle) Figure out if gcc makes any decisions about what is final or dynamic
        if (reference.getQualifiedName().equals("Object")
            || reference.getQualifiedName().equals("Class"))
        {
            reference.setDynamic(true);
        }

        classes.put(qualifiedName, reference);
    }

    public void addEnum(Node node, String qualifiedName)
    {
        if (classes.containsKey(qualifiedName))
        {
            err("Duplicate class, @enum conflict [" + qualifiedName + "]");
            return;
        }

        log("Model.addEnum(" + qualifiedName + ")");
        ClassReference reference = new ClassReference(this, node, qualifiedName);
        classes.put(qualifiedName, reference);
    }

    public void addTypeDef(Node node, String qualifiedName)
    {
        if (typedefs.containsKey(qualifiedName))
        {
            err("Duplicate @typedef [" + qualifiedName + "]");
            return;
        }

        log("Model.addTypeDef(" + qualifiedName + ")");

        ClassReference reference = new ClassReference(this, node, qualifiedName);
        typedefs.put(qualifiedName, reference);
    }

    public void addInterface(Node node, String qualifiedName)
    {
        if (classes.containsKey(qualifiedName))
        {
            err("Duplicate @interface [" + qualifiedName + "]");
            return;
        }

        log("Model.addInterface(" + qualifiedName + ")");

        ClassReference reference = new ClassReference(this, node, qualifiedName);
        classes.put(qualifiedName, reference);
    }

    public void addFinalClass(Node node, String qualifiedName)
    {
        if (classes.containsKey(qualifiedName))
        {
            err("Duplicate final class [" + qualifiedName + "]");
            return;
        }

        log("Model.addFinalClass(" + qualifiedName + ")");

        ClassReference reference = new ClassReference(this, node, qualifiedName);
        reference.setFinal(true);
        classes.put(qualifiedName, reference);
    }

    public void addFunction(Node node, String qualifiedName)
    {
        if (functions.containsKey(qualifiedName))
        {
            err("Duplicate global function [" + qualifiedName + "]");
            return;
        }

        log("Model.addFunction(" + qualifiedName + ")");

        FunctionReference reference = new FunctionReference(this, node, qualifiedName, node.getJSDocInfo());
        functions.put(qualifiedName, reference);
    }

    public boolean hasFunction(String functionName)
    {
        return functions.containsKey(functionName);
    }

    public boolean hasClass(String className)
    {
        return classes.containsKey(className);
    }

    public boolean hasConstant(String qualifiedName)
    {
        return constants.containsKey(qualifiedName);
    }

    public void addConstant(Node node, String qualifiedName)
    {
        if (constants.containsKey(qualifiedName))
        {
            // XXX Record warning;
            return;
        }

        log("Model.addConstant(" + qualifiedName + ")");

        ConstantReference reference = new ConstantReference(this, node, qualifiedName, node.getJSDocInfo());
        constants.put(qualifiedName, reference);
    }

    public void addConstantType(Node node, String qualifiedName, JSType type)
    {
        if (constants.containsKey(qualifiedName))
        {
            // XXX Record warning;
            return;
        }

        log("Model.addConstantType(" + qualifiedName + ")");

        ConstantReference reference = new ConstantReference(this, node, qualifiedName, node.getJSDocInfo(), type);
        constants.put(qualifiedName, reference);
    }

    public void addField(Node node, String className, String memberName)
    {
        ClassReference classReference = getClassReference(className);
        if (classReference != null)
            classReference.addField(node, memberName, node.getJSDocInfo(), false);
    }

    public void addStaticField(Node node, String className, String memberName)
    {
        ClassReference classReference = getClassReference(className);
        // XXX this is here because for now, the doc might be on the parent ASSIGN node
        // if it's a static property with a value
        JSDocInfo comment = NodeUtil.getBestJSDocInfo(node);
        if (classReference != null)
        {
            classReference.addField(node, memberName, comment, true);
        }
        else
        {
            err(">>>> {ReferenceModel} Class [" + className + "] not found in " + node.getSourceFileName());
        }
    }

    public void addMethod(Node node, String className, String memberName)
    {
        JSDocInfo comment = NodeUtil.getBestJSDocInfo(node);
        ClassReference classReference = getClassReference(className);
        if (classReference != null)
            classReference.addMethod(node, memberName, comment, false);
    }

    public void addStaticMethod(Node node, String className, String memberName)
    {
        ClassReference classReference = getClassReference(className);
        // XXX this is here because for now, the doc might be on the parent ASSIGN node
        // if it's a static property with a value
        JSDocInfo comment = NodeUtil.getBestJSDocInfo(node);
        if (classReference != null)
        {
            classReference.addMethod(node, memberName, comment, true);
        }
        else
        {
            err(">>>> {ReferenceModel} Class [" + className + "] not found in " + node.getSourceFileName());
        }
    }

    public final JSType evaluate(JSTypeExpression expression)
    {
        JSType jsType = null;

        if (expression != null)
        {
            try
            {
                jsType = expression.evaluate(null, jscompiler.getTypeRegistry());
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
        return jsType;
    }

    //--------------------------------------------------------------------------

    public ExcludedMember isExcludedClass(ClassReference classReference)
    {
        return getConfiguration().isExcludedClass(classReference);
    }

    public ExcludedMember isExcludedMember(ClassReference classReference, MemberReference memberReference)
    {
        return getConfiguration().isExcludedMember(classReference, memberReference);
    }

    //--------------------------------------------------------------------------

    protected void log(Node n)
    {
        DebugLogUtils.err(n);
    }

    protected void err(Node n)
    {
        DebugLogUtils.err(n);
    }

    protected void log(String message)
    {
        DebugLogUtils.log(message);
    }

    protected void err(String message)
    {
        DebugLogUtils.err(message);
    }

}
