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

import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.royale.compiler.common.ISourceLocation;
import org.apache.royale.compiler.internal.mxml.MXMLDialect;
import org.apache.royale.compiler.internal.parsing.ISourceFragment;
import org.apache.royale.compiler.internal.parsing.SourceFragment;
import org.apache.royale.compiler.internal.parsing.SourceFragmentsReader;
import org.apache.royale.compiler.internal.parsing.as.ASParser;
import org.apache.royale.compiler.internal.parsing.as.IProjectConfigVariables;
import org.apache.royale.compiler.internal.projects.RoyaleProject;
import org.apache.royale.compiler.internal.tree.as.ExpressionNodeBase;
import org.apache.royale.compiler.internal.tree.as.LiteralNode;
import org.apache.royale.compiler.internal.tree.as.NodeBase;
import org.apache.royale.compiler.internal.workspaces.Workspace;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.projects.ICompilerProject;
import org.apache.royale.compiler.tree.as.IASNode;
import org.apache.royale.compiler.tree.as.IExpressionNode;
import org.apache.royale.compiler.tree.as.ILiteralNode;
import org.apache.royale.compiler.tree.as.ILiteralNode.LiteralType;
import org.apache.royale.compiler.tree.mxml.IMXMLSingleDataBindingNode;
import org.apache.royale.compiler.tree.mxml.IMXMLNode;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;

/**
 * This class scans character-by-character across the logical text of multiple
 * source fragments (coming from attribute values or text units), looking for
 * databinding expressions using the same simple brace-matching algorithm as the
 * old compiler. It breaks fragments at each <code>{</code> that begins a
 * databinding and each <code>}</code> that ends one.
 * <p>
 * We have to deal with fragments because physical MXML constructs such as
 * entities, CDATA blocks, and MXML comments cannot be presented to the
 * ActionScript parser, but the logical ActionScript code that does get parsed
 * must produce ActionScript nodes that have the correct physical source
 * locations within the file.
 */
class MXMLDataBindingParser
{
    /**
     * An MXML databinding starts with this character.
     */
    private static final char LEFT_BRACE = '{';

    /**
     * An MXML databinding ends with this character.
     */
    private static final char RIGHT_BRACE = '}';

    /**
     * This character can be used to escape the '{' so that it doesn't indicate
     * databinding.
     */
    private static final char BACKSLASH = '\\';

    /**
     * Parses source fragments looking for databinding expressions.
     * <p>
     * If none are found, a String formed by concatenating the logical text of
     * the fragments is returned. If one databinding is found, with no
     * surrounding text, an {@code IMXMLDataBindingNode} representing it is
     * returned. If one or more databindings, with surrounding or interspersed
     * text is found, an {@code IMXMLConcatenatedDataBindingNode} is returned.
     */
    public static Object parse(IMXMLNode parent,
                               ISourceLocation sourceLocation,
                               ISourceFragment[] fragments,
                               Collection<ICompilerProblem> problems,
                               Workspace workspace,
                               MXMLDialect mxmlDialect,
                               ICompilerProject project)
    {
        assert fragments != null : "Expected an array of source fragments";

        // Find pairs of '{' and '}' in the fragments that represent databindings.
        ListMultimap<ISourceFragment, Integer> scanResult = scan(fragments);

        // A null result means there are no databindings.
        // Return the concatenated logical text.
        if (scanResult == null)
            return SourceFragmentsReader.concatLogicalText(fragments);

        // Split the fragments as necessary around the '{' and '}' that
        // begin and end databindings and produce a list of subfragment lists.
        // Each entry in the top-level list is either a DataBindingFragmentList
        // containing subfragments inside a databinding or a NonDataBindingFragmentList
        // containing subfragments outside a databinding.
        List<FragmentList> splitResult = split(fragments, scanResult);

        // Create an MXMLConcatenatedDataBindingNode with children.
        // Each DataBindingFragmentList creates a child MXMLDataBindingNode.
        // Each NonDataBindingFragmentList creates a child LiteralNode of type STRING.
        return createNode(parent, sourceLocation, splitResult, problems, workspace, mxmlDialect, project);
    }

    /**
     * Scans character-by-character across the logical text of each source
     * fragment, doing brace-matching and building up a list (in
     * dataBindingRanges) of DataBindingRange objects that keep track of where
     * each databinding starts and ends.
     */
    private static ListMultimap<ISourceFragment, Integer> scan(ISourceFragment[] fragments)
    {
        // We'll return a map mapping a source fragment to a list
        // of character indexes where the '{' and '}' for databindings
        // are located.
        ListMultimap<ISourceFragment, Integer> result = null;

        // This counter is incremented by each unescaped '{'
        // and decremented by each '}'.
        int nesting = 0;

        // These keep track of where we found a '{' that starts
        // a databinding, while we look for the matching '}'.
        ISourceFragment leftBraceFragment = null;
        int leftBraceCharIndex = -1;

        // This flag keeps track of whether we've seen a backslash,
        // which escapes the databinding meaning of '{'.
        boolean escape = false;

        // Iterate over each input fragment.
        for (ISourceFragment fragment : fragments)
        {
            // Iterate over each character of logical text in the current fragment.
            String text = fragment.getLogicalText();
            int n = text.length();
            for (int i = 0; i < n; i++)
            {
                switch (text.charAt(i))
                {
                    default:
                    {
                        escape = false;
                        break;
                    }

                    case LEFT_BRACE:
                    {
                        // If this '{' isn't nested and isn't escaped,
                        // it might start a databinding, so remember where it is.
                        if (nesting == 0 & !escape)
                        {
                            leftBraceFragment = fragment;
                            leftBraceCharIndex = i;
                        }
                        nesting++;
                        escape = false;
                        break;
                    }

                    case RIGHT_BRACE:
                    {
                        nesting--;
                        if (nesting == 0)
                        {
                            // We've found the matching '}' for the '{'.
                            // Record where the '{' and '}' are.
                            if (result == null)
                                result = LinkedListMultimap.<ISourceFragment, Integer> create();
                            result.put(leftBraceFragment, leftBraceCharIndex);
                            result.put(fragment, i);
                        }
                        escape = false;
                        break;
                    }

                    case BACKSLASH:
                    {
                        escape = true;
                        break;
                    }
                }
            }
        }

        return result;
    }

    /**
     * Builds a list of DataBindingFragmentList and NonDataBindingFragmentList
     * objects that organizes the fragments that are inside databindings and
     * outside databindings.
     */
    private static List<FragmentList> split(
            ISourceFragment[] fragments,
            ListMultimap<ISourceFragment, Integer> scanResult)
    {
        List<FragmentList> result = new ArrayList<FragmentList>();

        boolean inDataBinding = false;
        FragmentList currentList = null;

        for (ISourceFragment fragment : fragments)
        {
            // Get the character indexes where '{' and '}' for databindings are.
            List<Integer> braceIndices = scanResult.get(fragment);

            if (braceIndices == null)
            {
                if (currentList == null)
                    currentList = newFragmentList(result, inDataBinding);

                // If there is no '{' or '}' in this fragment,
                // add the fragment to the current fragment list.
                currentList.add(fragment);
            }
            else
            {
                // Otherwise, we'll have to break up the fragment.
                // Add an end-of-fragment index at the end of the list
                // so that we don't miss the last subfragment.
                braceIndices.add(fragment.getLogicalText().length());

                // Break up the fragment into subfragments around
                // the '{' and '}' characters.
                // Add each subfragment to the appropriate list.
                int beginIndex = 0;
                for (int braceIndex : braceIndices)
                {
                    ISourceFragment subfragment =
                            ((SourceFragment)fragment).subfragment(beginIndex, braceIndex);
                    if (subfragment != null)
                    {
                        currentList = newFragmentList(result, inDataBinding);
                        currentList.add(subfragment);
                    }
                    beginIndex = braceIndex + 1;
                    inDataBinding = !inDataBinding;
                }
            }
        }

        return result;
    }

    private static FragmentList newFragmentList(List<FragmentList> result, boolean inDataBinding)
    {
        FragmentList list = inDataBinding ?
                            new DataBindingFragmentList() :
                            new NonDataBindingFragmentList();

        result.add(list);

        return list;
    }

    private static IASNode createNode(IMXMLNode parent,
                                      ISourceLocation sourceLocation,
                                      List<FragmentList> listOfFragmentLists,
                                      Collection<ICompilerProblem> problems,
                                      Workspace workspace,
                                      MXMLDialect mxmlDialect,
                                      ICompilerProject project)
    {
        MXMLConcatenatedDataBindingNode node = new MXMLConcatenatedDataBindingNode((NodeBase)parent);

        node.setLocation(sourceLocation.getSourcePath(),
                         sourceLocation.getAbsoluteStart(), sourceLocation.getAbsoluteEnd(),
                         sourceLocation.getLine(), sourceLocation.getColumn());

        // Build a list of children for the MXMLConcatenatedDataBindingNode.
        List<IASNode> children = new ArrayList<IASNode>();

        for (List<ISourceFragment> fragmentList : listOfFragmentLists)
        {
            if (fragmentList instanceof DataBindingFragmentList)
            {
                // For each DataBindingFragmentList, add an IMXMLDataBindingNode
                // containing an IExpressionNode created by the ActionScript parser.
                children.add(createDataBindingNode(node, sourceLocation, fragmentList, problems, workspace, project));
            }
            else if (fragmentList instanceof NonDataBindingFragmentList)
            {
                // For each NonDataBindingFragmentList, add an ILiteralNode
                // of type STRING from the concatenated logical text.
                children.add(createStringLiteralNode(node, sourceLocation, fragmentList));
            }
        }

        // If no nodes were built, we probably had an empty databinding ( "{}" )
        // But we must make a node here, otherwise the tree will be malformed or
        // invalid. So let's create an empty one
        if (children.isEmpty())
        {
            assert listOfFragmentLists.isEmpty(); // should only happen if we
                                                  // were passed no frags
            children.add(createEmptyDatabindingNode(node, sourceLocation));
        }

        // If the leading/trailing child is a whitespace string literal node, remove it.
        trim(children, mxmlDialect);

        node.setChildren(children.toArray(new IASNode[0]));

        // If the only thing inside is a single IMXMLDataBindingNode,
        // return that, because we're not concatenating anything.
        if (node.getChildCount() == 1)
        {
            IASNode child = node.getChild(0);
            if (child instanceof IMXMLSingleDataBindingNode)
                return child;
        }

        // Otherwise, return the IConcatenatedDataBindingNode.
        return node;
    }

    private static ILiteralNode createStringLiteralNode(
            MXMLConcatenatedDataBindingNode parent,
            ISourceLocation sourceLocation,
            List<ISourceFragment> fragmentList)
    {
        ISourceFragment[] fragments = fragmentList.toArray(new ISourceFragment[0]);
        String text = SourceFragmentsReader.concatLogicalText(fragments);

        LiteralNode stringLiteralNode = new LiteralNode(LiteralType.STRING, text);
        stringLiteralNode.setParent(parent);

        ISourceFragment firstFragment = fragments[0];
        ISourceFragment lastFragment = fragments[fragments.length - 1];
        stringLiteralNode.setSourcePath(sourceLocation.getSourcePath());
        stringLiteralNode.setStart(firstFragment.getPhysicalStart());
        stringLiteralNode.setEnd(lastFragment.getPhysicalStart() + lastFragment.getPhysicalText().length());
        stringLiteralNode.setLine(firstFragment.getPhysicalLine());
        stringLiteralNode.setColumn(firstFragment.getPhysicalColumn());

        return stringLiteralNode;
    }

    private static IMXMLSingleDataBindingNode createDataBindingNode(
            IMXMLNode parent,
            ISourceLocation sourceLocation,
            List<ISourceFragment> fragments,
            Collection<ICompilerProblem> problems,
            Workspace workspace,
            ICompilerProject project)
    {
        MXMLSingleDataBindingNode result = new MXMLSingleDataBindingNode((NodeBase)parent);

        // Set location information for the MXMLDataBindingNode.
        ISourceFragment firstFragment = fragments.get(0);
        ISourceFragment lastFragment = fragments.get(fragments.size() - 1);
        result.setSourcePath(sourceLocation.getSourcePath());
        result.setStart(firstFragment.getPhysicalStart() - 1);
        result.setEnd(lastFragment.getPhysicalStart() + lastFragment.getPhysicalText().length() + 1);
        result.setLine(firstFragment.getPhysicalLine());
        result.setColumn(firstFragment.getPhysicalColumn() - 1);

        // Parse the fragments inside the databinding expression.
        Reader reader = new SourceFragmentsReader(sourceLocation.getSourcePath(), fragments.toArray(new ISourceFragment[0]));
        // IExpressionNode expressionNode = ASParser.parseDataBinding(workspace, reader, problems);
        IProjectConfigVariables projectConfigVariables =
            ((RoyaleProject)project).getProjectConfigVariables();
        IExpressionNode expressionNode = ASParser.parseExpression(workspace, reader, problems, 
                            projectConfigVariables, sourceLocation);

        // If the parse of the databinding expression failed,
        // substitute an empty string literal node
        // (which is the result of the empty databinding expression {}).
        if (expressionNode == null)
            expressionNode = new LiteralNode(LiteralType.STRING, "");

        // ASParser creates the ExpressionNodeBase as a child of a FileNode.
        // Make it a child of the MXMLDataBindingNode.
        ((ExpressionNodeBase)expressionNode).setParent(result);
        result.setExpressionNode(expressionNode);

        // double check that the node tree has its children's parent chain set up
        validateParents((NodeBase)expressionNode);
        return result;
    }

    private static void validateParents(NodeBase expressionNode)
    {
        for (int i = 0; i < expressionNode.getChildCount(); i++)
        {
            IASNode child = expressionNode.getChild(i);
            if (child instanceof NodeBase)
            {
                if (child.getParent() == null)
                    ((NodeBase)child).setParent((ExpressionNodeBase)expressionNode);
                validateParents((NodeBase)child);
            }
        }
    }
    
    // Makes a databinding node whose expression is just an empty string
    private static IMXMLSingleDataBindingNode createEmptyDatabindingNode(IMXMLNode parent, ISourceLocation sourceLocation)
    {
        MXMLSingleDataBindingNode result = new MXMLSingleDataBindingNode((NodeBase)parent);

        // Since we are empty, we can't set our source location based on our children.
        // But in the special case we can set our location to the same thing as our parent.
        result.setSourceLocation((NodeBase)parent);

        IExpressionNode expressionNode = new LiteralNode(LiteralType.STRING, "");
        ((ExpressionNodeBase)expressionNode).setParent(result);
        result.setExpressionNode(expressionNode);
        return result;
    }

    private static void trim(List<IASNode> children, MXMLDialect mxmlDialect)
    {
        assert (children != null && !children.isEmpty()); // function as written requires at least one child

        IASNode firstChild = children.get(0);
        removeIfWhitespace(children, firstChild, mxmlDialect);

        int n = children.size();
        IASNode lastChild = children.get(n - 1);
        removeIfWhitespace(children, lastChild, mxmlDialect);
    }

    private static void removeIfWhitespace(List<IASNode> children,
                                           IASNode child,
                                           MXMLDialect mxmlDialect)
    {
        if (child instanceof ILiteralNode)
        {
            String text = ((ILiteralNode)child).getValue();
            if (mxmlDialect.isWhitespace(text))
                children.remove(child);
        }
        assert !children.isEmpty(); // don't delete all the children
    }

    /**
     * This is a typedef-like class to improve readability by avoiding nested
     * parameterized types.
     */
    @SuppressWarnings("serial")
    private static abstract class FragmentList extends ArrayList<ISourceFragment>
    {
    }

    /**
     * This class is used to store sequences of {@code ISourceFragment}s that
     * are inside databinding expressions.
     */
    @SuppressWarnings("serial")
    private static class DataBindingFragmentList extends FragmentList
    {
    }

    /**
     * This class is used to store sequences of {@code ISourceFragment}s that
     * are outside databinding expressions.
     */
    @SuppressWarnings("serial")
    private static class NonDataBindingFragmentList extends FragmentList
    {
    }
}
