/*
 * 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 freemarker.core;

import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;

/**
 * An instruction that makes a single assignment, like [#local x=1].
 * This is also used as the child of {@link AssignmentInstruction}, if there are multiple assignments in the same tag,
 * like in [#local x=1 x=2].
 */
final class Assignment extends TemplateElement {

    // These must not clash with ArithmeticExpression.TYPE_... constants: 
    private static final int OPERATOR_TYPE_EQUALS = 0x10000;
    private static final int OPERATOR_TYPE_PLUS_EQUALS = 0x10001;
    private static final int OPERATOR_TYPE_PLUS_PLUS = 0x10002;
    private static final int OPERATOR_TYPE_MINUS_MINUS = 0x10003;
    
    private final int/*enum*/ scope;
    private final String variableName;
    private final int operatorType;
    private final Expression valueExp;
    private Expression namespaceExp;

    static final int NAMESPACE = 1;
    static final int LOCAL = 2;
    static final int GLOBAL = 3;
    
    private static final Number ONE = Integer.valueOf(1);

    /**
     * @param variableName the variable name to assign to.
     * @param valueExp the expression to assign.
     * @param scope the scope of the assignment, one of NAMESPACE, LOCAL, or GLOBAL
     */
    Assignment(String variableName,
            int operator,
            Expression valueExp,
            int scope) {
        this.scope = scope;
        
        this.variableName = variableName;
        
        if (operator == FMParserConstants.EQUALS) {
            operatorType = OPERATOR_TYPE_EQUALS;
        } else {
            switch (operator) {
            case FMParserConstants.PLUS_PLUS:
                operatorType = OPERATOR_TYPE_PLUS_PLUS;
                break;
            case FMParserConstants.MINUS_MINUS:
                operatorType = OPERATOR_TYPE_MINUS_MINUS;
                break;
            case FMParserConstants.PLUS_EQUALS:
                operatorType = OPERATOR_TYPE_PLUS_EQUALS;
                break;
            case FMParserConstants.MINUS_EQUALS:
                operatorType = ArithmeticExpression.TYPE_SUBSTRACTION;
                break;
            case FMParserConstants.TIMES_EQUALS:
                operatorType = ArithmeticExpression.TYPE_MULTIPLICATION;
                break;
            case FMParserConstants.DIV_EQUALS:
                operatorType = ArithmeticExpression.TYPE_DIVISION;
                break;
            case FMParserConstants.MOD_EQUALS:
                operatorType = ArithmeticExpression.TYPE_MODULO;
                break;
            default:
                throw new BugException();
            }
        }
        
        this.valueExp = valueExp;
    }
    
    void setNamespaceExp(Expression namespaceExp) {
        if (scope != NAMESPACE && namespaceExp != null) throw new BugException();
        this.namespaceExp =  namespaceExp;
    }

    @Override
    TemplateElement[] accept(Environment env) throws TemplateException {
        final Environment.Namespace namespace;
        if (namespaceExp == null) {
            switch (scope) {
            case LOCAL:
                namespace = null;
                break;
            case GLOBAL:
                namespace = env.getGlobalNamespace();
                break;
            case NAMESPACE:
                namespace = env.getCurrentNamespace();
                break;
            default:
                throw new BugException("Unexpected scope type: " + scope);
            }
        } else {
            TemplateModel uncheckedNamespace = namespaceExp.eval(env);
            try {
                namespace = (Environment.Namespace) uncheckedNamespace;
            } catch (ClassCastException e) {
                throw new NonNamespaceException(namespaceExp, uncheckedNamespace, env);
            }
            if (namespace == null) {
                throw InvalidReferenceException.getInstance(namespaceExp, env);
            }
        }
        
        TemplateModel value;
        if (operatorType == OPERATOR_TYPE_EQUALS) {
            value = valueExp.eval(env);
            if (value == null) {
                if (env.isClassicCompatible()) {
                    value = TemplateScalarModel.EMPTY_STRING;
                } else {
                    throw InvalidReferenceException.getInstance(valueExp, env);
                }
            }
        } else {
            TemplateModel lhoValue;
            if (namespace == null) {
                lhoValue = env.getLocalVariable(variableName);
            } else {
                lhoValue = namespace.get(variableName);
            }
            
            if (operatorType == OPERATOR_TYPE_PLUS_EQUALS) {  // Add or concat operation
                if (lhoValue == null) {
                    if (env.isClassicCompatible()) {
                        lhoValue = TemplateScalarModel.EMPTY_STRING;
                    } else {
                        throw InvalidReferenceException.getInstance(scope,
                                variableName, getOperatorTypeAsString(), env);
                    }
                }
                
                value = valueExp.eval(env);
                if (value == null) {
                    if (env.isClassicCompatible()) {
                        value = TemplateScalarModel.EMPTY_STRING;
                    } else {
                        throw InvalidReferenceException.getInstance(valueExp, env);
                    }
                }
                value = AddConcatExpression._eval(env, namespaceExp, null, lhoValue, valueExp, value);
            } else {  // Numerical operation
                Number lhoNumber;
                if (lhoValue instanceof TemplateNumberModel) {
                    lhoNumber = EvalUtil.modelToNumber((TemplateNumberModel) lhoValue, null);
                } else if (lhoValue == null) {
                    throw InvalidReferenceException.getInstance(scope, variableName, getOperatorTypeAsString(), env);
                } else {
                    throw new NonNumericalException(variableName, lhoValue, null, env);
                }

                if (operatorType == OPERATOR_TYPE_PLUS_PLUS) {
                    value  = AddConcatExpression._evalOnNumbers(env, getParentElement(), lhoNumber, ONE);
                } else if (operatorType == OPERATOR_TYPE_MINUS_MINUS) {
                    value = ArithmeticExpression._eval(
                            env, getParentElement(), lhoNumber, ArithmeticExpression.TYPE_SUBSTRACTION, ONE);
                } else { // operatorType == ArithmeticExpression.TYPE_...
                    Number rhoNumber = valueExp.evalToNumber(env);
                    value = ArithmeticExpression._eval(env, this, lhoNumber, operatorType, rhoNumber);
                }
            }
        }
        
        if (namespace == null) {
            env.setLocalVariable(variableName, value);
        } else {
            namespace.put(variableName, value);
        }
        return null;
    }

    @Override
    protected String dump(boolean canonical) {
        StringBuilder buf = new StringBuilder();
        String dn = getParentElement() instanceof AssignmentInstruction ? null : getNodeTypeSymbol();
        if (dn != null) {
            if (canonical) buf.append("<");
            buf.append(dn);
            buf.append(' ');
        }
        
        buf.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(variableName));
        
        if (valueExp != null) {
            buf.append(' ');
        }
        buf.append(getOperatorTypeAsString());
        if (valueExp != null) {
            buf.append(' ');
            buf.append(valueExp.getCanonicalForm());
        }
        if (dn != null) {
            if (namespaceExp != null) {
                buf.append(" in ");
                buf.append(namespaceExp.getCanonicalForm());
            }
            if (canonical) buf.append(">");
        }
        String result = buf.toString();
        return result;
    }
    
    @Override
    String getNodeTypeSymbol() {
        return getDirectiveName(scope);
    }
    
    static String getDirectiveName(int scope) {
        if (scope == Assignment.LOCAL) {
            return "#local";
        } else if (scope == Assignment.GLOBAL) {
            return "#global";
        } else if (scope == Assignment.NAMESPACE) {
            return "#assign";
        } else {
            return "#{unknown_assignment_type}";
        }
    }
    
    @Override
    int getParameterCount() {
        return 5;
    }

    @Override
    Object getParameterValue(int idx) {
        switch (idx) {
        case 0: return variableName;
        case 1: return getOperatorTypeAsString();
        case 2: return valueExp;
        case 3: return Integer.valueOf(scope);
        case 4: return namespaceExp;
        default: throw new IndexOutOfBoundsException();
        }
    }

    @Override
    ParameterRole getParameterRole(int idx) {
        switch (idx) {
        case 0: return ParameterRole.ASSIGNMENT_TARGET;
        case 1: return ParameterRole.ASSIGNMENT_OPERATOR;
        case 2: return ParameterRole.ASSIGNMENT_SOURCE;
        case 3: return ParameterRole.VARIABLE_SCOPE;
        case 4: return ParameterRole.NAMESPACE;
        default: throw new IndexOutOfBoundsException();
        }
    }

    @Override
    boolean isNestedBlockRepeater() {
        return false;
    }
    
    private String getOperatorTypeAsString() {
        if (operatorType == OPERATOR_TYPE_EQUALS) {
            return "=";
        } else if (operatorType == OPERATOR_TYPE_PLUS_EQUALS) {
            return "+=";
        } else if (operatorType == OPERATOR_TYPE_PLUS_PLUS) {
            return "++";
        } else if (operatorType == OPERATOR_TYPE_MINUS_MINUS) {
            return "--";
        } else {
            return ArithmeticExpression.getOperatorSymbol(operatorType) + "=";
        }
    }
    
    static String scopeAsString(int scope) {
        switch (scope) {
        case NAMESPACE: return "template namespace";
        case LOCAL: return "local scope";
        case GLOBAL: return "global scope";
        default: throw new AssertionError("Unsupported scope: " + scope);
        }
    }
    
}
