/**
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */
package org.richfaces.cdk.templatecompiler.el;

import static org.richfaces.cdk.templatecompiler.statements.HelperMethod.TO_STRING_CONVERSION;
import static org.richfaces.cdk.util.JavaUtils.getEscapedString;

import java.text.MessageFormat;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;

import org.jboss.el.parser.AstAnd;
import org.jboss.el.parser.AstBracketSuffix;
import org.jboss.el.parser.AstChoice;
import org.jboss.el.parser.AstCompositeExpression;
import org.jboss.el.parser.AstDeferredExpression;
import org.jboss.el.parser.AstDiv;
import org.jboss.el.parser.AstDynamicExpression;
import org.jboss.el.parser.AstEmpty;
import org.jboss.el.parser.AstEqual;
import org.jboss.el.parser.AstFalse;
import org.jboss.el.parser.AstFloatingPoint;
import org.jboss.el.parser.AstFunction;
import org.jboss.el.parser.AstGreaterThan;
import org.jboss.el.parser.AstGreaterThanEqual;
import org.jboss.el.parser.AstIdentifier;
import org.jboss.el.parser.AstInteger;
import org.jboss.el.parser.AstLessThan;
import org.jboss.el.parser.AstLessThanEqual;
import org.jboss.el.parser.AstLiteralExpression;
import org.jboss.el.parser.AstMethodSuffix;
import org.jboss.el.parser.AstMinus;
import org.jboss.el.parser.AstMod;
import org.jboss.el.parser.AstMult;
import org.jboss.el.parser.AstNegative;
import org.jboss.el.parser.AstNot;
import org.jboss.el.parser.AstNotEqual;
import org.jboss.el.parser.AstNull;
import org.jboss.el.parser.AstOr;
import org.jboss.el.parser.AstPlus;
import org.jboss.el.parser.AstPropertySuffix;
import org.jboss.el.parser.AstString;
import org.jboss.el.parser.AstTrue;
import org.jboss.el.parser.AstValue;
import org.jboss.el.parser.ELParser;
import org.jboss.el.parser.Node;
import org.richfaces.cdk.Logger;
import org.richfaces.cdk.templatecompiler.builder.model.JavaField;
import org.richfaces.cdk.templatecompiler.builder.model.JavaImport;
import org.richfaces.cdk.templatecompiler.builder.model.Variables;
import org.richfaces.cdk.templatecompiler.el.node.AstBracketSuffixTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstChoiceTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstDeferredOrDynamicExpressionTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstEmptyTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstFloatingPointTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstFunctionTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstIdentifierTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstIntegerTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstLiteralTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstMethodSuffixTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstNegativeTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstNotTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstPropertySuffixTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstStringTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.AstValueTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.BinaryArithmeticIntegerOperationTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.BinaryArithmeticOperationTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.BinaryBooleanOperationTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.BinaryBooleanResultOperationTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.ConstantValueTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.EqualityTestTreeNode;
import org.richfaces.cdk.templatecompiler.el.node.ITreeNode;
import org.richfaces.cdk.templatecompiler.el.types.ELPropertyDescriptor;
import org.richfaces.cdk.templatecompiler.el.types.ELType;
import org.richfaces.cdk.templatecompiler.el.types.TypesFactory;
import org.richfaces.cdk.templatecompiler.statements.HelperMethod;
import org.richfaces.cdk.templatecompiler.statements.StatementsContainer;
import org.richfaces.cdk.templatecompiler.statements.TypedTemplateStatement;

/**
 * Entry point for parsing EL expressions. @see parse() method.
 *
 * @author amarkhel
 */
public final class ELVisitor implements TypedTemplateStatement {
    private String parsedExpression = null;
    private ELType expressionType = null;
    private Variables variables = null;
    private Set<HelperMethod> usedHelperMethods = EnumSet.noneOf(HelperMethod.class);
    private final Logger log;
    private final TypesFactory typesFactory;
    private boolean mixedExpression;
    private boolean literal = true;
    private StatementsContainer parent;

    public ELVisitor(Logger log, TypesFactory typesFactory) {
        this.log = log;
        this.typesFactory = typesFactory;
    }

    public boolean isMixedExpression() {
        return mixedExpression;
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     *
     * @return the literal
     */
    @Override
    public boolean isLiteral() {
        return this.literal;
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     *
     * @param literal the literal to set
     */
    public void setLiteral(boolean literal) {
        this.literal = literal;
    }

    public ELType getType() {
        return expressionType;
    }

    public void setExpressionType(ELType variableType) {
        this.expressionType = variableType;
    }

    public ELType getVariable(String name) throws ParsingException {
        ELType variableType;
        if (variables.isDefined(name)) {
            variableType = variables.getVariable(name);
        } else {
            log.warn(MessageFormat.format("No type found in context for identifier ''{0}'', handling as generic Object", name));
            variableType = TypesFactory.OBJECT_TYPE;
        }

        return variableType;
    }

    /**
     * Parse specified EL expression and return Java code, that represent this expression
     *
     * @param expression - expression to resolve
     * @param contextMap - Map<String, Class<?>> - context for search classes.
     * @return generated Java code.
     * @throws ParsingException - if error occurred during parsing.
     */
    public void parse(String expression, Variables contextVariables, ELType expectedType) throws ParsingException {
        reset();

        Node ret = ELParser.parse(expression);
        variables = contextVariables;

        if (ret instanceof AstCompositeExpression && ret.jjtGetNumChildren() >= 2) {
            // AstCompositeExpression with 2+ children is a mixed expression
            usedHelperMethods.add(TO_STRING_CONVERSION);
            this.mixedExpression = true;
            this.literal = false;
        }

        if (ret != null && ret.jjtGetNumChildren() > 0) {
            parsedExpression = this.visit(ret);
        } else {
            parsedExpression = getEscapedString("");
            expressionType = TypesFactory.STRING_TYPE;
        }

        parsedExpression = coerceToType(parsedExpression, expectedType);
    }

    public String coerceToType(String valueString, ELType expectedType) {
        if (!expectedType.isAssignableFrom(getType())) {
            for (HelperMethod conversionMethod : HelperMethod.getConversionMethods()) {
                ELType returnType = typesFactory.getType(conversionMethod.getReturnType());
                if (expectedType.isAssignableFrom(returnType)) {
                    usedHelperMethods.add(conversionMethod);
                    setExpressionType(returnType);
                    return conversionMethod.getName() + "(" + valueString + ")";
                }
            }
            setLiteral(false);
        }

        return valueString;
    }

    private String visit(Node node) throws ParsingException {
        int numChildren = node.jjtGetNumChildren();
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < numChildren; i++) {
            Node child = node.jjtGetChild(i);

            ITreeNode treeNode = determineNodeType(child);

            treeNode.visit(sb, this);

            if (i != numChildren - 1) {
                sb.append(" + ");
            }
        }

        return sb.toString();
    }

    private void reset() {
        parsedExpression = null;
        usedHelperMethods.clear();
        variables = null;
        expressionType = null;
    }

    @Override
    public String getCode() {
        return parsedExpression;
    }

    @Override
    public Iterable<JavaImport> getRequiredImports() {
        return getType().getRequiredImports();
    }

    @Override
    public Iterable<JavaField> getRequiredFields() {
        return Collections.emptySet();
    }

    @Override
    public Iterable<HelperMethod> getRequiredMethods() {
        return usedHelperMethods;
    }

    public ELType getMatchingVisibleMethodReturnType(String methodName, ELType[] parameterTypes) throws ParsingException {

        return typesFactory.getMatchingVisibleMethodReturnType(getType(), methodName, parameterTypes);
    }

    public ELPropertyDescriptor getPropertyDescriptor(String propertyName) throws ParsingException {
        return typesFactory.getPropertyDescriptor(getType(), propertyName);
    }

    public void addHelperMethods(HelperMethod helper) {
        usedHelperMethods.add(helper);
    }

    /**
     * This method determine type of parsed node and create wrapper for them, that extends AbstractTreeNode. If node type is not
     * recognized - throws ParsingException.
     *
     * @param child - parsed node
     * @throws ParsingException - if node type is not recognized.
     * @return wrapper for parsed node(if node type is recognized), that implement ITreeNode interface.
     */
    public ITreeNode determineNodeType(Node child) throws ParsingException {
        ITreeNode treeNode = null;

        if (child instanceof AstIdentifier) {
            treeNode = new AstIdentifierTreeNode(child);
        } else if (child instanceof AstValue) {
            treeNode = new AstValueTreeNode(child);
        } else if (child instanceof AstInteger) {
            treeNode = new AstIntegerTreeNode(child);
        } else if (child instanceof AstString) {
            treeNode = new AstStringTreeNode(child);
        } else if (child instanceof AstFunction) {
            treeNode = new AstFunctionTreeNode(child);
        } else if (child instanceof AstDeferredExpression || child instanceof AstDynamicExpression) {
            treeNode = new AstDeferredOrDynamicExpressionTreeNode(child);
        } else if (child instanceof AstNot) {
            treeNode = new AstNotTreeNode(child);
        } else if (child instanceof AstChoice) {
            treeNode = new AstChoiceTreeNode(child);
        } else if (child instanceof AstEmpty) {
            treeNode = new AstEmptyTreeNode(child);
        } else if (child instanceof AstLiteralExpression) {
            treeNode = new AstLiteralTreeNode(child);
        } else if (child instanceof AstFalse) {
            treeNode = ConstantValueTreeNode.FALSE_NODE;
        } else if (child instanceof AstTrue) {
            treeNode = ConstantValueTreeNode.TRUE_NODE;
        } else if (child instanceof AstNull) {
            treeNode = ConstantValueTreeNode.NULL_NODE;
        } else if (child instanceof AstAnd) {
            treeNode = new BinaryBooleanOperationTreeNode(child, ELNodeConstants.AND_OPERATOR);
        } else if (child instanceof AstEqual) {
            treeNode = new EqualityTestTreeNode(child);
        } else if (child instanceof AstGreaterThan) {
            treeNode = new BinaryBooleanResultOperationTreeNode(child, ELNodeConstants.GREATER_THEN_OPERATOR);
        } else if (child instanceof AstGreaterThanEqual) {
            treeNode = new BinaryBooleanResultOperationTreeNode(child, ELNodeConstants.GREATER_THEN_OR_EQUALITY_OPERATOR);
        } else if (child instanceof AstLessThan) {
            treeNode = new BinaryBooleanResultOperationTreeNode(child, ELNodeConstants.LESS_THEN_OPERATOR);
        } else if (child instanceof AstLessThanEqual) {
            treeNode = new BinaryBooleanResultOperationTreeNode(child, ELNodeConstants.LESS_THEN_OR_EQUALITY_OPERATOR);
        } else if (child instanceof AstNotEqual) {
            treeNode = new EqualityTestTreeNode(child, true);
        } else if (child instanceof AstOr) {
            treeNode = new BinaryBooleanOperationTreeNode(child, ELNodeConstants.OR_OPERATOR);
        } else if (child instanceof AstDiv) {
            treeNode = new BinaryArithmeticOperationTreeNode(child, ELNodeConstants.DIV_OPERATOR);
        } else if (child instanceof AstMult) {
            treeNode = new BinaryArithmeticOperationTreeNode(child, ELNodeConstants.MULT_OPERATOR);
        } else if (child instanceof AstMod) {
            treeNode = new BinaryArithmeticIntegerOperationTreeNode(child, ELNodeConstants.MOD_OPERATOR);
        } else if (child instanceof AstPlus) {
            treeNode = new BinaryArithmeticOperationTreeNode(child, ELNodeConstants.PLUS_OPERATOR);
        } else if (child instanceof AstMinus) {
            treeNode = new BinaryArithmeticOperationTreeNode(child, ELNodeConstants.MINUS_OPERATOR);
        } else if (child instanceof AstBracketSuffix) {
            treeNode = new AstBracketSuffixTreeNode(child);
        } else if (child instanceof AstNegative) {
            treeNode = new AstNegativeTreeNode(child);
        } else if (child instanceof AstFloatingPoint) {
            treeNode = new AstFloatingPointTreeNode(child);
        } else if (child instanceof AstMethodSuffix) {
            treeNode = new AstMethodSuffixTreeNode(child);
        } else if (child instanceof AstPropertySuffix) {
            treeNode = new AstPropertySuffixTreeNode(child);
        } else {
            throw new ParsingException("Node " + child.getClass().getSimpleName() + "[" + child.getImage()
                    + "] is not recognized;");
        }

        return treeNode;
    }

    @Override
    public void setParent(StatementsContainer parent) {
        this.parent = parent;
    }
}
