/*
 *
 *  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.flex.compiler.internal.tree.mxml;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.flex.compiler.asdoc.IASDocComment;
import org.apache.flex.compiler.common.IEmbedResolver;
import org.apache.flex.compiler.definitions.IClassDefinition;
import org.apache.flex.compiler.definitions.IDefinition;
import org.apache.flex.compiler.definitions.IFunctionDefinition;
import org.apache.flex.compiler.filespecs.IFileSpecification;
import org.apache.flex.compiler.internal.css.codegen.CSSCompilationSession;
import org.apache.flex.compiler.internal.definitions.ClassDefinition;
import org.apache.flex.compiler.internal.parsing.as.OffsetLookup;
import org.apache.flex.compiler.internal.projects.FlexProject;
import org.apache.flex.compiler.internal.scopes.ASFileScope;
import org.apache.flex.compiler.internal.scopes.MXMLFileScope;
import org.apache.flex.compiler.internal.targets.ITargetAttributes;
import org.apache.flex.compiler.internal.targets.TargetAttributesMap;
import org.apache.flex.compiler.internal.tree.as.FunctionNode;
import org.apache.flex.compiler.internal.tree.as.ImportNode;
import org.apache.flex.compiler.internal.units.MXMLCompilationUnit;
import org.apache.flex.compiler.mxml.IMXMLData;
import org.apache.flex.compiler.mxml.IMXMLInstructionData;
import org.apache.flex.compiler.mxml.IMXMLTagData;
import org.apache.flex.compiler.mxml.IMXMLTextData;
import org.apache.flex.compiler.mxml.IMXMLTextData.TextType;
import org.apache.flex.compiler.mxml.IMXMLUnitData;
import org.apache.flex.compiler.problems.ICompilerProblem;
import org.apache.flex.compiler.problems.MXMLConstructorHasParametersProblem;
import org.apache.flex.compiler.problems.MXMLContentAfterRootTagProblem;
import org.apache.flex.compiler.problems.MXMLContentBeforeRootTagProblem;
import org.apache.flex.compiler.problems.MXMLFinalClassProblem;
import org.apache.flex.compiler.problems.MXMLMissingRootTagProblem;
import org.apache.flex.compiler.problems.MXMLMultipleRootTagsProblem;
import org.apache.flex.compiler.problems.MXMLNotAClassProblem;
import org.apache.flex.compiler.problems.MXMLUnresolvedTagProblem;
import org.apache.flex.compiler.problems.MXMLXMLProcessingInstructionLocationProblem;
import org.apache.flex.compiler.projects.ICompilerProject;
import org.apache.flex.compiler.scopes.IASScope;
import org.apache.flex.compiler.tree.ASTNodeID;
import org.apache.flex.compiler.tree.as.IASNode;
import org.apache.flex.compiler.tree.as.IDefinitionNode;
import org.apache.flex.compiler.tree.as.IFileNode;
import org.apache.flex.compiler.tree.as.IFileNodeAccumulator;
import org.apache.flex.compiler.tree.as.IImportNode;
import org.apache.flex.compiler.tree.as.IScopedNode;
import org.apache.flex.compiler.tree.mxml.IMXMLDocumentNode;
import org.apache.flex.compiler.tree.mxml.IMXMLFileNode;
import org.apache.flex.compiler.tree.mxml.IMXMLStyleNode;

public class MXMLFileNode extends MXMLNodeBase implements IMXMLFileNode, IScopedNode, IFileNode, IFileNodeAccumulator
{
    // TODO Make this class package internal rather than public.

    public MXMLFileNode()
    {
        super(null);
        styleNodes = new ArrayList<IMXMLStyleNode>();
        importNodes = new LinkedList<IImportNode>();
        embedNodes = new LinkedList<IEmbedResolver>();
        requiredResourceBundles = new HashSet<String>();
    }

    private FlexProject project;

    private String qname;

    private MXMLFileScope fileScope;

    /**
     * The node corresponding to the root tag of the file. If the root tag is
     * missing, cannot be resolved, or resolves to something other than a
     * non-final class, the documentNode will be null. If it exists, the
     * documentNode is the one and only child of this file node. If the
     * documentNode does not exist, then this file node has no children.
     */
    private MXMLDocumentNode documentNode;

    private long includeTreeLastModified;

    private final List<IImportNode> importNodes;

    private final List<IEmbedResolver> embedNodes;

    private final Set<String> requiredResourceBundles;

    // Translates offset between local and absolute.
    private OffsetLookup offsetLookup;

    private final List<IMXMLStyleNode> styleNodes;

    private ITargetAttributes targetAttributes;

    private CSSCompilationSession cssCompilationSession;

    void initialize(MXMLTreeBuilder builder)
    {
        IFileSpecification fileSpec = builder.getFileSpecification();

        project = builder.getProject();
        qname = builder.getQName();
        fileScope = builder.getFileScope();

        // When parsing the MXML for AST construction, ad-hoc IncludeHandler are created in
        // order to please the MXML lexer and to keep the AST node offsets in-sync with
        // the scope tree node offsets. As a result, the MXML AST nodes does not have a 
        // usable OffsetLookup after tree building. That's why we need to provide the real
        // OffsetLookup from the scope building.
        final OffsetLookup offsetLookup = fileScope.getOffsetLookup();
        assert offsetLookup != null : "Expected offset lookup on MXMLFileScope.";
        this.offsetLookup = offsetLookup;

        setLocation(builder);

        // Add implicit import nodes for ActionScript.
        for (String importStr : ASFileScope.getImplicitImportsForAS())
        {
            ImportNode implicitImportNode = new MXMLImplicitImportNode(project, importStr);
            addImportNode(implicitImportNode);
        }

        // Add implicit import nodes for MXML.
        for (IImportNode implicitImportNode : project.getImplicitImportNodesForMXML(builder.getMXMLDialect()))
        {
            addImportNode(implicitImportNode);
        }

        includeTreeLastModified = fileSpec.getLastModified();

        processUnits(builder);
    }

    private void processUnits(MXMLTreeBuilder builder)
    {
        IMXMLData mxmlData = builder.getMXMLData();
        
        // add problems that were encountered while parsing the data
        for (ICompilerProblem problem : mxmlData.getProblems())
        {
            builder.addProblem(problem);
        }

        if (mxmlData.getNumUnits() == 0)
            return;

        boolean foundRootTag = false;
        IMXMLTextData asDoc = null;

        // Walk the top-level units of the MXMLData.
        for (IMXMLUnitData unit = mxmlData.getUnit(0); unit != null; unit = unit.getNextSiblingUnit())
        {
            if (unit instanceof IMXMLInstructionData)
            {
                if (unit.getStart() > 0)
                {
                    ICompilerProblem problem = new MXMLXMLProcessingInstructionLocationProblem(unit);
                    builder.addProblem(problem);
                }
            }
            else if (unit instanceof IMXMLTagData)
            {
                if (!foundRootTag)
                {
                    foundRootTag = true;
                    processRootTag(builder, (IMXMLTagData)unit, asDoc);
                }
                else
                {
                    ICompilerProblem problem = new MXMLMultipleRootTagsProblem(unit);
                    builder.addProblem(problem);
                }
            }
            else if (unit instanceof IMXMLTextData)
            {
                IMXMLTextData textData = (IMXMLTextData)unit;
                if (textData.getTextType().equals(TextType.ASDOC))
                    asDoc = textData;
                if (!builder.getMXMLDialect().isWhitespace(textData.getCompilableText()))
                {
                    if (documentNode == null)
                    {
                        ICompilerProblem problem = new MXMLContentBeforeRootTagProblem(unit);
                        builder.addProblem(problem);
                    }
                    else
                    {
                        ICompilerProblem problem = new MXMLContentAfterRootTagProblem(unit);
                        builder.addProblem(problem);
                    }
                }
            }
        }

        if (!foundRootTag)
        {
            ICompilerProblem problem = new MXMLMissingRootTagProblem(builder.getPath());
            builder.addProblem(problem);
        }
    }

    private void processRootTag(MXMLTreeBuilder builder, IMXMLTagData rootTag, IMXMLTextData asDoc)
    {
        ClassDefinition fileDef = fileScope.getMainClassDefinition();
        assert fileDef != null;

        IDefinition tagDef = builder.getFileScope().resolveTagToDefinition(rootTag);

        // Report a problem if the root tag doesn't map to a definition.
        if (tagDef == null)
        {
            ICompilerProblem problem = new MXMLUnresolvedTagProblem(rootTag);
            builder.addProblem(problem);
            return;
        }

        // Report a problem if that definition isn't for a class.
        if (!(tagDef instanceof IClassDefinition))
        {
            ICompilerProblem problem = new MXMLNotAClassProblem(rootTag, tagDef.getQualifiedName());
            builder.addProblem(problem);
            return;
        }

        IClassDefinition tagDefinition = (IClassDefinition)tagDef;

        // Report a problem if that class is final.
        if (tagDefinition != null && tagDefinition.isFinal())
        {
            ICompilerProblem problem = new MXMLFinalClassProblem(rootTag, tagDef.getQualifiedName());
            builder.addProblem(problem);
            return;
        }

        // Report a problem is that class's constructor has required parameters.
        IFunctionDefinition constructor = tagDefinition.getConstructor();
        if (constructor != null && constructor.hasRequiredParameters())
        {
            ICompilerProblem problem = new MXMLConstructorHasParametersProblem(rootTag, tagDef.getQualifiedName());
            builder.addProblem(problem);
            return;
        }

        documentNode = new MXMLDocumentNode(this);
        documentNode.setClassReference(project, tagDefinition);
        documentNode.setClassDefinition(fileDef);
        documentNode.initializeFromTag(builder, rootTag);
        
        if (asDoc != null)
        {
            IASDocComment asDocComment = builder.getWorkspace().getASDocDelegate().createASDocComment(asDoc, tagDefinition);
            documentNode.setASDocComment(asDocComment);
        }

        fileDef.setNode(documentNode);
        // TODO setNode() sets the nameStart and nameEnd to -1
        // Fix setNode() to set the values properly
        // From MXMLScopeBuilder - CM clients expect the name start of the root class definition to be at 0.
        if (fileDef.getNameStart() == -1 && fileDef.getNameEnd() == -1)
            fileDef.setNameLocation(0, 0);
    }

    @Override
    public IASNode getChild(int i)
    {
        return i == 0 ? documentNode : null;
    }

    @Override
    public int getChildCount()
    {
        return documentNode != null ? 1 : 0;
    }

    @Override
    public ASTNodeID getNodeID()
    {
        return ASTNodeID.MXMLFileID;
    }

    @Override
    public String getName()
    {
        return qname;
    }

    @Override
    public IMXMLDocumentNode getDocumentNode()
    {
        return documentNode;
    }

    @Override
    public IASScope getScope()
    {
        return fileScope;
    }

    @Override
    public void getAllImports(Collection<String> imports)
    {
        for (IImportNode node : importNodes)
        {
            imports.add(node.getImportName());
        }
    }

    @Override
    public void getAllImportNodes(Collection<IImportNode> imports)
    {
        imports.addAll(importNodes);
    }

    @Override
    public IDefinition[] getTopLevelDefinitions(boolean includeDefinitionsOutsideOfPackage,
                                                boolean includeNonPublicDefinitions)
    {
        return documentNode != null ?
                new IDefinition[] {documentNode.getClassDefinition()} :
                new IDefinition[] {};
    }

    @Override
    public IDefinitionNode[] getTopLevelDefinitionNodes(boolean includeDefinitionsOutsideOfPackage,
                                                        boolean includeNonPublicDefinitions)
    {
        return documentNode != null ?
                new IDefinitionNode[] {documentNode} :
                new IDefinitionNode[] {};
    }

    @Override
    public boolean hasIncludes()
    {
        return fileScope.getOffsetLookup().hasIncludes();
    }

    public IClassDefinition[] getLibraryDefinitions()
    {
        return ((MXMLFileScope)getScope()).getLibraryDefinitions();
    }

    @Override
    public IFileSpecification getFileSpecification()
    {
        return fileScope.getWorkspace().getFileSpecification(fileScope.getContainingPath());
    }

    @Override
    public ICompilerProject getCompilerProject()
    {
        return project;
    }

    /**
     * For debugging only. Builds a string such as <code>"C:\test.mxml"</code>
     * from the path of the file node.
     */
    @Override
    protected boolean buildInnerString(StringBuilder sb)
    {
        sb.append('"');
        sb.append(getSourcePath());
        sb.append('"');

        return true;
    }

    /**
     * @param lastModified
     */
    protected void updateIncludeTreeLastModified(long lastModified)
    {
        this.includeTreeLastModified = Math.max(lastModified, this.includeTreeLastModified);

    }

    /**
     * @return A time stamp of the last modification time this filenode's
     * include tree
     */
    @Override
    public long getIncludeTreeLastModified()
    {
        return this.includeTreeLastModified;
    }

    // TODO: this method should should be merged with FileNode.addImportNode()
    //       if the two MXMLFileNodes are merged
    @Override
    public void addImportNode(IImportNode node)
    {
        importNodes.add(node);
    }

    @Override
    public List<IImportNode> getImportNodes()
    {
        return importNodes;
    }

    // TODO: this method should should be merged with FileNode.addEmbedNode()
    //       if the two MXMLFileNodes are merged
    @Override
    public void addEmbedNode(IEmbedResolver node)
    {
        embedNodes.add(node);
    }

    // TODO: this method should should be merged with FileNode.getEmbedNodes()
    //       if the two MXMLFileNodes are merged
    @Override
    public List<IEmbedResolver> getEmbedNodes()
    {
        return embedNodes;
    }

    // TODO: this method should should be merged with FileNode.addRequiredResourceBundle()
    //       if the two MXMLFileNodes are merged
    @Override
    public void addRequiredResourceBundle(String bundleName)
    {
        requiredResourceBundles.add(bundleName);
    }

    // TODO: this method should should be merged with FileNode.getRequiredResourceBundles()
    //       if the two MXMLFileNodes are merged
    @Override
    public Set<String> getRequiredResourceBundles()
    {
        return requiredResourceBundles;
    }

    @Override
    public OffsetLookup getOffsetLookup()
    {
        return this.offsetLookup;
    }

    private void setLocation(MXMLTreeBuilder builder)
    {
        String sourcePath = builder.getPath();

        MXMLCompilationUnit cu = builder.getCompilationUnit();
        IMXMLData mxmlData = builder.getMXMLData();

        // Find the extents of the file node by finding the last bit
        // of the MXMLData. Note that this assumes that MXMLData
        // represents trailing white space (which it currently does).
        // Note also that we can't just look at the file system,
        // because the MXMLFileNode might actually backed by an
        // in-memory document, not the original file.
        final String absoluteFilename = cu.getAbsoluteFilename();
        final int mxmlDataEnd = mxmlData.getEnd();
        // Convert from a local offset to an absolute offset
        final int[] possibleAbsoluteOffsets =
                offsetLookup.getAbsoluteOffset(absoluteFilename, mxmlDataEnd);
        assert possibleAbsoluteOffsets.length == 1 : "Expected only 1 match.";
        final int absoluteEnd = possibleAbsoluteOffsets[0];
        setLocation(sourcePath, 0, absoluteEnd, 1, 1);
    }

    /**
     * Assuming {@code MXMLStyleNode} is always a direct child of
     * {@link MXMLDocumentNode}.
     */
    @Override
    public List<IMXMLStyleNode> getStyleNodes()
    {
        return styleNodes;
    }

    @Override
    public ITargetAttributes getTargetAttributes(final ICompilerProject project)
    {
        if (targetAttributes == null && documentNode != null)
            targetAttributes = new TargetAttributesMap(documentNode.rootAttributes);
        return targetAttributes;
    }

    /**
     * Get the CSS semantic information.
     * 
     * @return CSS semantic information.
     */
    @Override
    public synchronized CSSCompilationSession getCSSCompilationSession()
    {
        if (cssCompilationSession == null)
            cssCompilationSession = project.getCSSCompilationSession();
        return cssCompilationSession;
    }

    @Override
    public void addDeferredFunctionNode(FunctionNode functionNode)
    {
        throw new UnsupportedOperationException("Functions should never be deferred in MXML document.");
    }

    @Override
    public void populateFunctionNodes()
    {
        // Do nothing. Function bodies are never deferred for non-AS documents.
    }
    
    @Override
    public Collection<ICompilerProblem> getProblems()
    {
        return Collections.emptyList();
    }
}
