/*
 * 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.web.el.completion;

import com.sun.el.parser.Node;
import java.util.HashSet;
import java.util.Set;
import javax.el.ELException;
import org.netbeans.modules.el.lexer.api.ELTokenId;
import org.netbeans.modules.web.el.ELElement;
import org.netbeans.modules.web.el.ELParser;
import org.netbeans.modules.web.el.ELPreprocessor;
import org.openide.util.Pair;

/**
 * Attempts to sanitize EL statements. Check the unit test
 * for finding out what cases are currently handled.
 *
 * @author Erno Mononen
 */
public final class ELSanitizer {

    static final String ADDED_SUFFIX = "x"; // NOI18N
    static final String ADDED_QUOTED_SUFFIX = "'x'"; // NOI18N
    private final ELPreprocessor expression;
    private final ELElement element;
    private static final Set<Pair<ELTokenId, ELTokenId>> BRACKETS;
    private final int relativeOffset;

    static {
        BRACKETS = new HashSet<>();
        BRACKETS.add(Pair.of(ELTokenId.LBRACKET, ELTokenId.RBRACKET));
        BRACKETS.add(Pair.of(ELTokenId.LPAREN, ELTokenId.RPAREN));
    }

    public ELSanitizer(ELElement element, int relativeOffset) {
        this.element = element;
        this.expression = element.getExpression();
        this.relativeOffset = relativeOffset;
    }

    /**
     * Attempts to sanitize the contained element.
     * @return Returns a
     * sanitized copy of the element if sanitization was successful, otherwise
     * the element itself. In other words, the returned element is <strong>not</strong>
     * guaranteed to be valid.
     */
    public ELElement sanitized() {
        try {
            String sanitizedExpression = sanitize(expression.getOriginalExpression(), relativeOffset); //use original expression!
            ELPreprocessor elp = new ELPreprocessor(sanitizedExpression, ELPreprocessor.XML_ENTITY_REFS_CONVERSION_TABLE);
            Node sanitizedNode = ELParser.parse(elp);
            return element.makeValidCopy(sanitizedNode, elp);
        } catch (ELException ex) {
            return element;
        }
    }

    // package private for unit tests
    static String sanitize(final String expression) {
        return sanitize(expression, -1);
    }
    
    static String sanitize(final String expression, int relativeOffset) {
        boolean closingCurlyBracketAdded = false;
        String copy = expression;
        if (!expression.endsWith("}")) {
            copy += "}";
            closingCurlyBracketAdded = true;
        }
        CleanExpression cleanExpression = CleanExpression.getCleanExression(copy);
        if (cleanExpression == null) {
            return expression;
        }
        //the CleanExpression removed the #{ or ${ prefix
        relativeOffset -= 2;
        
        String result = cleanExpression.clean;
        if (closingCurlyBracketAdded && relativeOffset >= 0) {
            result = secondPass(result, relativeOffset);
        }
        if (result.trim().isEmpty()) {
            result += ADDED_SUFFIX;
        }

        // resolve completion invoked within the EL
        if (relativeOffset > 0 && relativeOffset < result.length()) {
            String exprEnd = result.substring(relativeOffset);
            String exprStart = result.substring(0, relativeOffset);
            result = thirdPass(exprStart, exprEnd) + exprEnd;
        } else {
            result = thirdPass(result, ""); //NOI18N
        }
        return cleanExpression.prefix + result + cleanExpression.suffix;
    }

    //unclosed expressions handling
    private static String secondPass(String expression, int relativeOffset) {
        //Cut everything after up to the relative offset (caret)
        return expression.substring(0, relativeOffset);
    }

    private static String thirdPass(String expression, String ending) {
        String spaces = "";
        if (expression.endsWith(" ")) {
            int lastNonWhiteSpace = findLastNonWhiteSpace(expression);
            if (lastNonWhiteSpace > 0) {
                spaces = expression.substring(lastNonWhiteSpace + 1);
                expression = expression.substring(0, lastNonWhiteSpace + 1);
            }
        }

        if (!expression.isEmpty()) {
            char lastChar = expression.charAt(expression.length() - 1);
            if (lastChar == '\'' || lastChar == '"') { //NOI18N
                expression += lastChar;
            }
        }

        for (ELTokenId elToken : ELTokenId.values()) {
            if (elToken.fixedText() == null || !expression.endsWith(elToken.fixedText())) {
                continue;
            }
            // special handling for brackets
            for (Pair<ELTokenId, ELTokenId> bracket : BRACKETS) {
                if (expression.endsWith(bracket.first().fixedText())) {
                    if (expression.endsWith(ELTokenId.LBRACKET.fixedText())) {
                        return expression + ADDED_QUOTED_SUFFIX + bracket.second().fixedText();
                    }
                    return expression + bracket.second().fixedText();
                } else if (expression.endsWith(bracket.second().fixedText())) {
                    if (expression.endsWith(ELTokenId.RBRACKET.fixedText())) {
                        // e.g. #{bean.items[|]}
                        return expression.substring(0, expression.length() - 1) + ADDED_QUOTED_SUFFIX + ELTokenId.RBRACKET.fixedText();
                    } else if (expression.endsWith(ELTokenId.DOT.fixedText() + ELTokenId.RPAREN.fixedText())
                        // for opened classname call - e.g. #{(java.|)}
                            || expression.endsWith(ELTokenId.LAMBDA.fixedText() + ELTokenId.RPAREN.fixedText())) {
                            // for started lambda expression - e.g. #{[1,4].stream().peek(i->|)}
                        return expression.substring(0, expression.length() - 1) + ADDED_SUFFIX + ELTokenId.RPAREN.fixedText();
                    }
                }
            }
            // sanitizes cases where the expressions ends with dot and spaces,
            // e.g. #{foo.  }
            if (ELTokenId.DOT == elToken) {
                if (unbalancedLeftParen(expression + ending)) {
                    return expression + ADDED_SUFFIX + ELTokenId.RPAREN.fixedText() + spaces ;
                } else {
                    return expression + ADDED_SUFFIX + spaces ;
                }
            }

            // for COLON - e.g. #{foo:
            if (ELTokenId.COLON == elToken) {
                return expression + ADDED_SUFFIX + ELTokenId.LPAREN.fixedText()
                        + ELTokenId.RPAREN.fixedText() + spaces;
            }

            // for operators
            if (ELTokenId.ELTokenCategories.OPERATORS.hasCategory(elToken)) {
                return expression + spaces + ADDED_SUFFIX;
            }
            if (ELTokenId.ELTokenCategories.KEYWORDS.hasCategory(elToken)) {
                return expression + " " + spaces + ADDED_SUFFIX;
            }
        }

        // for COLON - e.g. #{foo:foo
        if (expression.contains(ELTokenId.COLON.fixedText())) {
            return expression + ELTokenId.LPAREN.fixedText() + ELTokenId.RPAREN.fixedText() + spaces;
        }

        if (unbalancedLeftBracket(expression)) {
            return expression + ELTokenId.RBRACKET.fixedText();
        }

        return expression + spaces;
    }

    private static boolean unbalancedLeftBracket(String expression) {
        return (expression.indexOf(ELTokenId.LBRACKET.fixedText()) > expression.indexOf(ELTokenId.RBRACKET.fixedText()));
    }

    private static boolean unbalancedLeftParen(String expression) {
        return (expression.indexOf(ELTokenId.LPAREN.fixedText()) > expression.indexOf(ELTokenId.RPAREN.fixedText()));
    }

    // package private for tests
    static int findLastNonWhiteSpace(String str) {
        int lastNonWhiteSpace = -1;
        for (int i = str.length() - 1; i >= 0; i--) {
            if (!Character.isWhitespace(str.charAt(i))) {
                lastNonWhiteSpace = i;
                break;
            }
        }
        return lastNonWhiteSpace;
    }

    private static class CleanExpression {

        private final String clean, prefix, suffix;

        public CleanExpression(String clean, String prefix, String suffix) {
            this.clean = clean;
            this.prefix = prefix;
            this.suffix = suffix;
        }

        private static CleanExpression getCleanExression(String expression) {
            if ((expression.startsWith("#{") || expression.startsWith("${"))
                    && expression.endsWith("}")) {

                String prefix = expression.substring(0, 2);
                String clean = expression.substring(2, expression.length() - 1);
                String suffix = expression.substring(expression.length() - 1);
                return new CleanExpression(clean, prefix, suffix);
            }
            return null;
        }
    }
}
