/*
 * 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.html.angular;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.html.lexer.HTMLTokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.modules.html.angular.index.AngularJsController;
import org.netbeans.modules.html.angular.index.AngularJsIndex;
import org.netbeans.modules.html.angular.index.AngularJsIndexer;
import org.netbeans.modules.html.angular.model.AngularModel;
import org.netbeans.modules.html.angular.model.Directive;
import org.netbeans.modules.html.editor.api.gsf.HtmlParserResult;
import org.netbeans.modules.html.editor.spi.embedding.JsEmbeddingProviderPlugin;
import org.netbeans.modules.parsing.api.Embedding;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.web.common.api.LexerUtils;
import org.netbeans.modules.web.common.api.WebUtils;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;

/**
 *
 * @author Petr Pisl, mfukala@netbeans.org
 * @author Roman Svitanic
 */
@MimeRegistration(mimeType = "text/html", service = JsEmbeddingProviderPlugin.class)
public class AngularJsEmbeddingProviderPlugin extends JsEmbeddingProviderPlugin {

    private static class StackItem {

        final String tag;
        String finishText;

        public StackItem(String tag) {
            this.tag = tag;
            this.finishText = ""; //NOI18N
        }
        
        public void addFinishText(String text) {
            finishText = text + finishText;
        }
    }
    
    private final LinkedList<StackItem> stack;
    
    private TokenSequence<HTMLTokenId> tokenSequence;
    private Snapshot snapshot;
    private List<Embedding> embeddings;
    private int processedTemplate;
  
    private Directive interestedAttr;
    /** keeps mapping from simple property name to the object fqn 
     */
    private HashMap<String, String> propertyToFqn;
    
    public AngularJsEmbeddingProviderPlugin() {
        this.stack = new LinkedList();
        this.propertyToFqn = new HashMap();
    }

    @Override
    public boolean startProcessing(HtmlParserResult parserResult, Snapshot snapshot, TokenSequence<HTMLTokenId> tokenSequence, List<Embedding> embeddings) {
        if (AngularJsIndexer.Factory.isScannerThread()) {
            return false;
        }
        this.snapshot = snapshot;
        this.tokenSequence = tokenSequence;
        this.embeddings = embeddings;
        this.stack.clear();
        this.processedTemplate = -1;
        AngularModel model = AngularModel.getModel(parserResult);
        if(!model.isAngularPage()) {
            return false;
        }
        
        FileObject file = snapshot.getSource().getFileObject();
        if (file == null) {
            return false;
        }
        
        return true;
    }

    @Override
    public void endProcessing() {
        super.endProcessing(); //To change body of generated methods, choose Tools | Templates.
        if (processedTemplate > 0) {
            for (int i = 0; i < processedTemplate; i++) {
                embeddings.add(snapshot.create("}\n});\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
            }
        }
    }
    
    

    @Override
    public boolean processToken() {
        boolean processed = false;
        CharSequence tokenText = tokenSequence.token().text();        
        switch (tokenSequence.token().id()) {
            case TAG_OPEN:
                stack.push(new StackItem(tokenText.toString()));
                break;
            case TAG_CLOSE:
                if (!stack.isEmpty()) {
                    StackItem top = stack.pop();
                    while (!stack.isEmpty() && !LexerUtils.equals(top.tag, tokenText, false, false)) {
                        if (!top.finishText.isEmpty()) {
                            embeddings.add(snapshot.create(top.finishText, Constants.JAVASCRIPT_MIMETYPE));
                        }
                        top = stack.pop();
                    }
                    if (!top.finishText.isEmpty()) {
                        embeddings.add(snapshot.create(top.finishText, Constants.JAVASCRIPT_MIMETYPE));
                    }
                }
                break;
            case ARGUMENT:
                Directive ajsDirective = Directive.getDirective(tokenText.toString().trim().toLowerCase());
                if(ajsDirective != null) {
                    interestedAttr = ajsDirective;
                } else {
                    interestedAttr = null;
                }
                break;
            case VALUE:
                 if (interestedAttr != null) {
                    String value = WebUtils.unquotedValue(tokenText);
                    switch (interestedAttr) {
                        case controller:
                            processed = processController(value);
                            stack.peek().addFinishText("}\n});\n"); //NOI18N
                            break;
                        case model:
                        case disabled:
                        case click:
                            processed = processModel(value);
                            break;
                        case repeat:
                        case repeatStart:
                            processed = processRepeat(value);
                            stack.peek().addFinishText("}\n"); //NOI18N
                            break;
                        case modelOptions:
                        case link:
                            processed = processObject(value);
                            break;
                        case options:
                            processed = processComprehensionExpression(value);
                            stack.peek().addFinishText("}\n"); //NOI18N
                            break;
                        default:   
                            processed = processExpression(value);
                    }
                }
                break;
            case EL_OPEN_DELIMITER:
                 if (tokenSequence.moveNext()) {
                    if (tokenSequence.token().id() == HTMLTokenId.EL_CONTENT) {
                        String value = tokenSequence.token().text().toString();
                        int indexStart = 0;
                        boolean parenRemoved = false;
                        // check if one-time binding "{{::expr}}" is present
                        int oneTimeBindingShift = 0;                        
                        if (value.trim().startsWith("::")) { //NOI18N
                            // remove double colon
                            int doubleColonIndex = value.indexOf("::");
                            value = value.substring(doubleColonIndex + 2);
                            oneTimeBindingShift = doubleColonIndex + 2;
                        }
                        String name = value.trim();
                        if (value.startsWith("(")) {
                            name = value.substring(1);
                            parenRemoved = true;
                            indexStart = 1;
                        }
                        int parenIndex = name.indexOf('('); //NOI18N
                        if (parenIndex > -1) {
                            name = name.substring(0, parenIndex);
                        }
                        processTemplate();
                        if (propertyToFqn.containsKey(name)) {
                            embeddings.add(snapshot.create(propertyToFqn.get(name) + ".$scope.", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                            embeddings.add(snapshot.create(tokenSequence.offset() + oneTimeBindingShift, value.length(), Constants.JAVASCRIPT_MIMETYPE));
                            embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                            processed = true;
                        } else if (!name.contains("|") && !name.contains(":")){ //NOI18N
                            embeddings.add(snapshot.create(tokenSequence.offset() + oneTimeBindingShift, value.length(), Constants.JAVASCRIPT_MIMETYPE));
                            embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                            processed = true;
                        } else if (name.contains("|")){
                            int indexEnd = name.indexOf('|');
                            name = name.substring(0, indexEnd);
                            if (parenRemoved) {
                                indexEnd = name.lastIndexOf(')');
                                if (indexEnd > -1) {
                                    name = name.substring(0, indexEnd);
                                }
                            }
                            if (name.startsWith("-")) {
                                indexStart++;
                                name = name.substring(1);
                            }
                            if(propertyToFqn.containsKey(name)) {
                                embeddings.add(snapshot.create(propertyToFqn.get(name) + ".$scope.", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                                embeddings.add(snapshot.create(tokenSequence.offset() + indexStart + oneTimeBindingShift, name.length(), Constants.JAVASCRIPT_MIMETYPE));
                                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                                processed = true;
                            } else {
                                embeddings.add(snapshot.create(tokenSequence.offset() + indexStart + oneTimeBindingShift, name.length(), Constants.JAVASCRIPT_MIMETYPE));
                                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                                processed = true;
                            }
                        } else {
                            tokenSequence.movePrevious();
                        }
                    } else if (tokenSequence.token().id() == HTMLTokenId.EL_CLOSE_DELIMITER) {
                        embeddings.add(snapshot.create(tokenSequence.offset(), 0, Constants.JAVASCRIPT_MIMETYPE));
                        processed = true;
                    } else {
                        tokenSequence.movePrevious();
                    }
                }
                break;    
            default:
        }
        return processed;
    }
    
    private boolean processController(String controllerName) {
        processTemplate();
        StringBuilder sb = new StringBuilder();
        Project project = FileOwnerQuery.getOwner(snapshot.getSource().getFileObject());
        AngularJsIndex index = null;
        try {
            index = AngularJsIndex.get(project);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }

        if (controllerName.contains(" as ")) { //NOI18N
            // we have controllerAs expression
            String[] parts = controllerName.trim().split(" as "); //NOI18N
            if (parts.length < 2) {
                return false;
            }

            sb.append("(function () {\nvar "); //NOI18N

            String fqn = parts[0].trim();
            if (index != null) {
                Collection<AngularJsController> controllers = index.getControllers(parts[0], true);
                for (AngularJsController controller : controllers) {
                    if (controller.getName().equals(parts[0])) {
                        fqn = controller.getFqn();
                        break;
                    }
                }
            }

            embeddings.add(snapshot.create(sb.toString(), Constants.JAVASCRIPT_MIMETYPE));
            sb = new StringBuilder();

            if (!fqn.isEmpty()) {
                int propNameOffset = controllerName.indexOf(parts[1].trim());
                embeddings.add(snapshot.create(tokenSequence.offset() + 1 + propNameOffset, parts[1].trim().length(), Constants.JAVASCRIPT_MIMETYPE));
                sb.append(" = new "); //NOI18N
                sb.append(fqn);
                sb.append(";\n"); //NOI18N
            }

            embeddings.add(snapshot.create(sb.toString(), Constants.JAVASCRIPT_MIMETYPE));
            sb = new StringBuilder();
            if (!parts[0].isEmpty()) {
                embeddings.add(snapshot.create(tokenSequence.offset() + 1, parts[0].length(), Constants.JAVASCRIPT_MIMETYPE));
                sb.append(";");
            } else {
                embeddings.add(snapshot.create(tokenSequence.offset() + 1, 0, Constants.JAVASCRIPT_MIMETYPE));
            }
            sb.append("\n{ \n"); //NOI18N
            embeddings.add(snapshot.create(sb.toString(), Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
            return true;
        } else {
            // classic controller with $scope
            sb.append("(function () {\nvar $scope = "); //NOI18N

            String fqn = controllerName.trim();
            if (index != null) {
                Collection<AngularJsController> controllers = index.getControllers(controllerName, true);
                for (AngularJsController controller : controllers) {
                    if (controller.getName().equals(controllerName)) {
                        fqn = controller.getFqn();
                        break;
                    }
                }
            }

            if (!fqn.isEmpty()) {
                sb.append(fqn);
                sb.append(".");
            }
            sb.append("$scope;\n");   //NOI18N
            embeddings.add(snapshot.create(sb.toString(), Constants.JAVASCRIPT_MIMETYPE));
            sb = new StringBuilder();
            final int embeddingEndOffset = tokenSequence.offset() + 1 + controllerName.length();
            if (!controllerName.isEmpty() && embeddingEndOffset <= snapshot.getText().length()) {
                embeddings.add(snapshot.create(tokenSequence.offset() + 1, controllerName.length(), Constants.JAVASCRIPT_MIMETYPE));
                sb.append(";");
            } else {
                embeddings.add(snapshot.create(tokenSequence.offset() + 1, 0, Constants.JAVASCRIPT_MIMETYPE));
            }
            sb.append("\nwith ($scope) { \n");
            embeddings.add(snapshot.create(sb.toString(), Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
            return true;
        }
    }
    
    private boolean processModel(String value) { 
        processTemplate();
        if (value.isEmpty()) {
            embeddings.add(snapshot.create("( function () {", Constants.JAVASCRIPT_MIMETYPE));
            embeddings.add(snapshot.create(tokenSequence.offset() + 1, 0, Constants.JAVASCRIPT_MIMETYPE));
            embeddings.add(snapshot.create(";})();\n", Constants.JAVASCRIPT_MIMETYPE));
        } else {
             embeddings.add(snapshot.create(tokenSequence.offset() + 1, 
                     value.length() - (tokenSequence.index() == tokenSequence.tokenCount() - 1 ? 1 : 0) , Constants.JAVASCRIPT_MIMETYPE));
             embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
//            int parenStart = value.indexOf('('); //NOI18N
//            String name = value;
//            int nameStart = 0;
//            int lenght = name.length();
//            if (parenStart > -1) {
//                name = name.substring(0, parenStart).trim();
//            }
//            if (name.indexOf('=') > -1) {
//                name = name.substring(0, name.indexOf('=')).trim();
//            }
//            if (name.charAt(0) == '!') {
//                name = name.substring(1);
//                nameStart = 1;
//            }
//            if (propertyToFqn.containsKey(name)) {
//                embeddings.add(snapshot.create(propertyToFqn.get(name) + ".$scope.", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
//                
//                if(parenStart > -1) {
//                    int parenEnd = parenStart;
//                    int balance = 1;
//                    while (balance > 0 && parenEnd < value.length()) {
//                        char ch = value.charAt(parenEnd);
//                        if (ch == '(') {
//                            balance++;
//                        } else if (ch == ')') {
//                            balance--;
//                        }
//                        if (balance > 0) {
//                            parenEnd++;
//                        }
//                    }
//                    embeddings.add(snapshot.create(tokenSequence.offset() + 1, parenEnd, Constants.JAVASCRIPT_MIMETYPE));
//                } else {
//                    embeddings.add(snapshot.create(tokenSequence.offset() + 1, lenght, Constants.JAVASCRIPT_MIMETYPE));
//                } 
//                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
//            }  else {
//                // need to create local variable
//                if (value.indexOf(' ') == -1 && parenStart == -1 && value.indexOf('.') == -1) {
//                    embeddings.add(snapshot.create("var ", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
//                }
//                embeddings.add(snapshot.create(tokenSequence.offset() + 1 + nameStart, value.length() - nameStart, Constants.JAVASCRIPT_MIMETYPE));
//                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
//            }
        }
        return true;
    }

    private boolean processObject(String value) {
        processTemplate();
        embeddings.add(snapshot.create("( function () {\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
        if (value.isEmpty()) {
            embeddings.add(snapshot.create(tokenSequence.offset() + 1, 0, Constants.JAVASCRIPT_MIMETYPE));
        } else {
            embeddings.add(snapshot.create("var value = ", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
            embeddings.add(snapshot.create(tokenSequence.offset() + 1, value.length(), Constants.JAVASCRIPT_MIMETYPE));
        }
        embeddings.add(snapshot.create(";\n})();\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
        return true;
    }

    private boolean processRepeat(String expression) {
        processTemplate();
        boolean processed = false;

        String repeatExpression = expression;

        // split the expression with "track by"
        // if present, it should be the last thing in expression
        String[] trackByParts = expression.split(" track by "); //NOI18N
        if (trackByParts.length == 2) {
            repeatExpression = trackByParts[0];
        }

        // split the expression with "as"
        // if present, it should be now the last thing in expression (after we have taken care of "track by")
        String[] asParts = repeatExpression.split(" as "); //NOI18N
        if (asParts.length == 2) {
            repeatExpression = asParts[0];
        }

        processed = processRepeatLoop(repeatExpression, 0);

        // now insert the embeddings for "as" alias expression and/or "track by" tracking expression
        if (asParts.length == 2) {
            String asPropName = asParts[1].trim();
            int asPropNameOffset = expression.indexOf(asPropName);
            embeddings.add(snapshot.create(tokenSequence.offset() + 1 + asPropNameOffset, asPropName.length(), Constants.JAVASCRIPT_MIMETYPE));
            embeddings.add(snapshot.create(" = [];\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
        }
        if (trackByParts.length == 2) {
            String trackingExpr = trackByParts[1].trim();
            int trackingExprOffset = expression.indexOf(trackingExpr);
            embeddings.add(snapshot.create(tokenSequence.offset() + 1 + trackingExprOffset, trackingExpr.length(), Constants.JAVASCRIPT_MIMETYPE));
            embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
        }

        return processed;
    }

    /**
     * Processes the main part (loop itself) of the repeat expression and
     * filters. E.g. "item in items" or "item in items | filter:searchText".
     *
     * @param expression loop expression to process, stripped of all leading
     * "group by", "as" or "disable when" expressions. It can contain filters
     * @param initialOffset offset shift from the original expression in
     * directive's value. For "ng-repeat" it should be always 0. This means that
     * no parts were stripped from the beginning of the original value
     * @return {@code true} if cycle has been processed, {@code false} otherwise
     */
    private boolean processRepeatLoop(String expression, int initialOffset) {
        boolean processedCycle = false;

        // split the expression with |
        // we expect that the first part is the for cycle and the rest are conditions
        // and attributes like orderby, filter etc.
        String[] parts = expression.split("\\|"); //NOI18N
        if (parts.length > 0) {
            // try to create the for cycle in virtual source
            if (parts[0].contains(" in ")) {
                // we understand only "in"  now
                String[] forParts = parts[0].trim().split(" in ");   // NOI18N
                if (forParts.length < 2) {
                    return false;
                }
                embeddings.add(snapshot.create("for (var ", Constants.JAVASCRIPT_MIMETYPE));
                // one-time binding in repeat expression (e.g., ng-repeat="item in ::items")
                boolean oneTimeBinding = false;
                int oneTimeBindingShift = 0;
                if (forParts[1].trim().startsWith("::")) { //NOI18N
                    // one-time binding was found in repeat expression, remove double colon from collection name
                    int doubleColonIndex = forParts[1].indexOf("::"); //NOI18N
                    forParts[1] = forParts[1].substring(doubleColonIndex + 2);
                    oneTimeBindingShift = doubleColonIndex + 2;
                    oneTimeBinding = true;
                }
                // forParts keeps value, collection
                // now need to check, whether the value is simple or (key, value) - issue #230223
                if (!forParts[0].contains(",")) {
                    // create virtual source for simple case:  value in collection
                    if (forParts.length == 2 && propertyToFqn.containsKey(forParts[1])) {
                        // if we know the collection from a controller ....
                        int lastPartPos = expression.indexOf(forParts[1]); // the start position of the collection name
                        embeddings.add(snapshot.create(tokenSequence.offset() + 1 + initialOffset, lastPartPos - oneTimeBindingShift, Constants.JAVASCRIPT_MIMETYPE));
                        embeddings.add(snapshot.create(propertyToFqn.get(forParts[1]) + ".$scope.", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                        embeddings.add(snapshot.create(tokenSequence.offset() + 1 + lastPartPos + initialOffset, forParts[1].length(), Constants.JAVASCRIPT_MIMETYPE));
                    } else {
                        if (oneTimeBinding) {
                            // we don't know the collection from controller and we have one-time binding present
                            int lastPartPos = expression.indexOf(forParts[1]); // the start position of the collection name
                            embeddings.add(snapshot.create(tokenSequence.offset() + 1 + initialOffset, lastPartPos - oneTimeBindingShift, Constants.JAVASCRIPT_MIMETYPE));
                            embeddings.add(snapshot.create(tokenSequence.offset() + 1 + lastPartPos + initialOffset, forParts[1].length(), Constants.JAVASCRIPT_MIMETYPE));
                        } else {
                            // if we don't know the collection from a controller, put it to the virtual source as it is
                            embeddings.add(snapshot.create(tokenSequence.offset() + 1 + initialOffset, parts[0].length(), Constants.JAVASCRIPT_MIMETYPE));
                        }
                    }
                    embeddings.add(snapshot.create(") {\n", Constants.JAVASCRIPT_MIMETYPE));  //NOI18N
                } else {
                    // expect that thre is expression like: (key, value) in collection
                    // such expression should be translated to: for (var key in collectoin) { var value = collection[key];
                    String valueExp = forParts[0].trim();
                    if (valueExp.startsWith("(")) {     // NOI18N
                        valueExp = valueExp.substring(1);
                    }
                    if (valueExp.endsWith(")")) {   // NOI18N
                        valueExp = valueExp.substring(0, valueExp.length() - 1);
                    }
                    valueExp = valueExp.trim();
                    String[] keyValue = valueExp.split(",");    // NOI18N

                    int lastPartPos = expression.indexOf(forParts[1]); // the start position of the collection name
                    int keyPos = expression.indexOf(keyValue[0]);
                    // map the key name
                    embeddings.add(snapshot.create(tokenSequence.offset() + 1 + keyPos + initialOffset, keyValue[0].length(), Constants.JAVASCRIPT_MIMETYPE));
                    if (keyValue.length == 1) {
                        // add a comma after variable name to trigger the error if an identifier for value is missing
                        // e.g., ng-repeat="(key, ) in expression" instead of ng-repeat="(key, value) in expression"
                        embeddings.add(snapshot.create(",", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                    }
                    // map " in " 
                    embeddings.add(snapshot.create(" in ", Constants.JAVASCRIPT_MIMETYPE));  //NOI18N
                    if (forParts.length == 2 && propertyToFqn.containsKey(forParts[1])) {
                        // if we know the collection from a controller ....
                        // map the collection
                        embeddings.add(snapshot.create(propertyToFqn.get(forParts[1]) + ".$scope.", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                        embeddings.add(snapshot.create(tokenSequence.offset() + 1 + lastPartPos + initialOffset, forParts[1].length(), Constants.JAVASCRIPT_MIMETYPE));
                    } else {
                        // map the collection
                        embeddings.add(snapshot.create(tokenSequence.offset() + 1 + lastPartPos + initialOffset, forParts[1].length(), Constants.JAVASCRIPT_MIMETYPE));
                    }
                    if (keyValue.length == 2) {
                        // both identfiers, for key and value are present: ng-repeat="(key, value) in expression"
                        embeddings.add(snapshot.create(") {\nvar ", Constants.JAVASCRIPT_MIMETYPE));    //NOI18N
                        int valuePos = expression.indexOf(keyValue[1]);
                        embeddings.add(snapshot.create(tokenSequence.offset() + 1 + valuePos + initialOffset, keyValue[1].length(), Constants.JAVASCRIPT_MIMETYPE));
                        embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE));      //NOI18N
                    } else {
                        embeddings.add(snapshot.create(") {\n", Constants.JAVASCRIPT_MIMETYPE));      //NOI18N
                    }
                }
                // the for cycle should be closed in appropriate CLOSE_TAG token
                processedCycle = true;
            }
            int partIndex = 1;
            int lastPartPos = parts[0].length() + 1;
            while (partIndex < parts.length) { // are there any condition / attributes of the cycle?
                if (parts[partIndex].contains(":")) {
                    String[] conditionParts = parts[partIndex].trim().split(":");
                    if (conditionParts.length > 1) {
                        String propName = conditionParts[1].trim();
                        int indexInName = 0;
                        char ch = propName.charAt(indexInName);
                        while ((indexInName < (propName.length() - 1)) && (Character.isWhitespace(ch) || ch == '{')) {
                            ch = propName.charAt(++indexInName);
                        }
                        if (indexInName > 0) {
                            propName = propName.substring(indexInName);
                        }
                        int position = lastPartPos + parts[partIndex].indexOf(propName) + 1;
                        indexInName = propName.length() - 1;
                        ch = propName.charAt(indexInName);
                        while (indexInName > 0 && (Character.isWhitespace(ch) || ch == '{' || ch == '}')) {
                            ch = propName.charAt(--indexInName);
                        }
                        if (indexInName < (propName.length() - 1)) {
                            propName = propName.substring(0, indexInName - 1);
                        }
                        if (propertyToFqn.containsKey(propName)) {
                            embeddings.add(snapshot.create(propertyToFqn.get(propName) + ".$scope.", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N                            
                        }
                        embeddings.add(snapshot.create(tokenSequence.offset() + position + initialOffset, propName.length(), Constants.JAVASCRIPT_MIMETYPE));
                        embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                    }
                }
                lastPartPos = lastPartPos + parts[partIndex].length() + 1;
                partIndex++;
            }
        }

        return processedCycle;
    }

    private boolean processExpression(String value) {
        processTemplate();
        boolean processed = false;
        if (value.isEmpty()) {
            embeddings.add(snapshot.create("( function () {", Constants.JAVASCRIPT_MIMETYPE));
            embeddings.add(snapshot.create(tokenSequence.offset() + 1, 0, Constants.JAVASCRIPT_MIMETYPE));
            embeddings.add(snapshot.create(";})();\n", Constants.JAVASCRIPT_MIMETYPE));
            processed = true;
        } else {
            int lastPartPos = 0;
            int valueTrimPos = 0;
            int oneTimeBindingShift = 0;
            boolean oneTimeBinding = false;
            if (value.trim().startsWith("::")) { //NOI18N
                // one-time binding "::expr" was found, remove double colon from expression
                int doubleColonIndex = value.indexOf("::");
                value = value.substring(doubleColonIndex + 2);
                oneTimeBindingShift = doubleColonIndex + 2;
                oneTimeBinding = true;
            }
            if (value.startsWith("{")) {
                value = value.substring(1);
                lastPartPos = 1;
            }
            String valueTrim = value.trim();
            if (valueTrim.endsWith("}")) {
                value = valueTrim.substring(0, valueTrim.length() - 1);
                valueTrim = "";
            } else if (valueTrim.contains("}")) {
                valueTrimPos = valueTrim.indexOf('}');
                value = valueTrim.substring(0, valueTrimPos);
                valueTrim = valueTrim.substring(valueTrimPos + 1);
            } else {
                valueTrim = "";
            }
            int index = value.indexOf(':'); // are there pairs like name: expr?
            if (index > -1) {
                String[] parts = value.split(","); // example: ng-class="{completed: todo.completed, editing: todo == editedTodo}"
                for (String part : parts) {
                    index = value.indexOf(':');
                    if (index > 0) {
                        String[] conditionParts = part.trim().split(":");
                        if (conditionParts.length > 1 && !conditionParts[0].contains("?")) { //ignore if ternary operator is present (issue #251057)
                            String propName = conditionParts[1].trim();
                            if (!propName.isEmpty() && !propName.startsWith("//")) {
                                int position = lastPartPos + part.indexOf(propName) + oneTimeBindingShift + 1;
                                if (propName.charAt(0) == '"' || propName.charAt(0) == '\'') {
                                    propName = propName.substring(1);
                                    position++;
                                    if (propName.endsWith("\"") || propName.endsWith("'")) {
                                        propName = propName.substring(0, propName.length() - 1);
                                    }
                                }
                                if (propertyToFqn.containsKey(propName)) {
                                    embeddings.add(snapshot.create(propertyToFqn.get(propName) + ".$scope.", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N                            
                                }
                                embeddings.add(snapshot.create(tokenSequence.offset() + position, propName.length(), Constants.JAVASCRIPT_MIMETYPE));
                                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                                processed = true;
                            }
                        }
                    }
                    lastPartPos = lastPartPos + part.length() + 1;
                }
            }
            if (!valueTrim.isEmpty()) {
                embeddings.add(snapshot.create(tokenSequence.offset() + lastPartPos + oneTimeBindingShift + 1, valueTrim.length(), Constants.JAVASCRIPT_MIMETYPE));
                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
            }
            if (oneTimeBinding && !processed) {
                // we have one-time binding expression "::expr" which hasn't been processed yet
                embeddings.add(snapshot.create(tokenSequence.offset() + oneTimeBindingShift + 1, value.length(), Constants.JAVASCRIPT_MIMETYPE));
                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
                processed = true;
            }
        }
        return processed;
    }

    private boolean processComprehensionExpression(String expression) {
        processTemplate();
        boolean processed = false;

        String comprehensionExpression = expression;

        // split the expression with "track by"
        // if present, it should be the last thing in expression
        String[] trackByParts = expression.split(" track by "); //NOI18N
        if (trackByParts.length == 2) {
            comprehensionExpression = trackByParts[0];
        }

        String[] forKeywordParts = comprehensionExpression.split(" for "); //NOI18N
        if (forKeywordParts.length == 2) {
            comprehensionExpression = forKeywordParts[1];
            int initialOffset = forKeywordParts[0].length() + 5; // 5 is length of " for "

            processed = processRepeatLoop(comprehensionExpression, initialOffset);

            String tmpStr = forKeywordParts[0].trim();
            if (tmpStr.contains(" disable when ")) { //NOI18N
                String[] disableWhenParts = tmpStr.split(" disable when "); //NOI18N
                tmpStr = disableWhenParts[0].trim();

                int disableWhenOffset = expression.indexOf(disableWhenParts[1]);
                embeddings.add(snapshot.create(tokenSequence.offset() + 1 + disableWhenOffset, disableWhenParts[1].trim().length(), Constants.JAVASCRIPT_MIMETYPE));
                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
            }
            if (tmpStr.contains(" group by ")) { //NOI18N
                String[] groupByParts = tmpStr.split(" group by "); //NOI18N
                tmpStr = groupByParts[0].trim();

                int groupByOffset = expression.indexOf(groupByParts[1]);
                embeddings.add(snapshot.create(tokenSequence.offset() + 1 + groupByOffset, groupByParts[1].trim().length(), Constants.JAVASCRIPT_MIMETYPE));
                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
            }
            if (tmpStr.contains(" as ")) { //NOI18N
                String[] asParts = tmpStr.split(" as "); //NOI18N
                tmpStr = asParts[0].trim();

                int asOffset = expression.indexOf(asParts[1]);
                embeddings.add(snapshot.create(tokenSequence.offset() + 1 + asOffset, asParts[1].trim().length(), Constants.JAVASCRIPT_MIMETYPE));
                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
            }

            int firstItemOffset = expression.indexOf(tmpStr);
            embeddings.add(snapshot.create(tokenSequence.offset() + 1 + firstItemOffset, tmpStr.trim().length(), Constants.JAVASCRIPT_MIMETYPE));
            embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N

            if (trackByParts.length == 2) {
                String trackingExpr = trackByParts[1].trim();
                int trackingExprOffset = expression.indexOf(trackingExpr);
                embeddings.add(snapshot.create(tokenSequence.offset() + 1 + trackingExprOffset, trackingExpr.length(), Constants.JAVASCRIPT_MIMETYPE));
                embeddings.add(snapshot.create(";\n", Constants.JAVASCRIPT_MIMETYPE)); //NOI18N
            }
        }

        return processed;
    }

    private void processTemplate() {
         if (processedTemplate > -1) {
            // was already processed
            return;
        }
        processedTemplate = 0;
        FileObject fo = snapshot.getSource().getFileObject();
        Project project = FileOwnerQuery.getOwner(fo);
        AngularJsIndex index = null;
        try {
             index = AngularJsIndex.get(project);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        if (index != null) {
            Collection<AngularJsController.ModuleConfigRegistration> controllersForTemplate = index.getControllersForTemplate(fo.toURI());
            for (AngularJsController.ModuleConfigRegistration controllerRegistration : controllersForTemplate) {
                Collection<AngularJsController> controllers = index.getControllers(controllerRegistration.getControllerName(), true);
                if (!controllers.isEmpty()) {
                    String fqn;
                    for (AngularJsController controller : controllers) {
                        if (controller.getName().equals(controllerRegistration.getControllerName())) {
                            fqn = controller.getFqn();
                            processedTemplate++;
                            StringBuilder sb = new StringBuilder();
                            if (controllerRegistration.getControllerAsName() == null) {
                                // classic Angular controller with $scope
                                sb.append("(function () {\nvar $scope = "); //NOI18N
                                if (!fqn.isEmpty()) {
                                    sb.append(fqn);
                                } else {
                                    sb.append(controllerRegistration.getControllerName());
                                }
                                sb.append("."); //NOI18N
                                sb.append("$scope;\n");   //NOI18N
                                sb.append("\nwith ($scope) { \n"); //NOI18N
                            } else {
                                // we have controllerAs present in configuration
                                sb.append("(function () {\nvar "); //NOI18N
                                sb.append(controllerRegistration.getControllerAsName()).append(" = new "); //NOI18N
                                if (!fqn.isEmpty()) {
                                    sb.append(fqn);
                                    sb.append(";\n"); //NOI18N
                                } else {
                                    sb.append(controllerRegistration.getControllerName());
                                    sb.append(";\n"); //NOI18N
                                }
                                sb.append("{ \n"); //NOI18N
                            }
                            embeddings.add(snapshot.create(sb.toString(), Constants.JAVASCRIPT_MIMETYPE));
                            break;
                        }
                    }
                } else {
                    processedTemplate++;
                    StringBuilder sb = new StringBuilder();
                    if (controllerRegistration.getControllerAsName() == null) {
                        // classic Angular controller with $scope
                        sb.append("(function () {\nvar $scope = "); //NOI18N
                        sb.append(controllerRegistration.getControllerName());
                        sb.append("."); //NOI18N
                        sb.append("$scope;\n");   //NOI18N
                        sb.append("\nwith ($scope) { \n"); //NOI18N
                    } else {
                        // we have controllerAs present in configuration
                        sb.append("(function () {\nvar "); //NOI18N
                        sb.append(controllerRegistration.getControllerAsName());
                        sb.append(" = new "); //NOI18N
                        sb.append(controllerRegistration.getControllerName());
                        sb.append(";\n"); //NOI18N
                        sb.append("{ \n"); //NOI18N
                    }
                    embeddings.add(snapshot.create(sb.toString(), Constants.JAVASCRIPT_MIMETYPE));
                }
            }
        }
    }
}
