/* ====================================================================
   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.poi.ss.formula;

import org.apache.poi.ss.formula.ptg.AbstractFunctionPtg;
import org.apache.poi.ss.formula.ptg.AttrPtg;
import org.apache.poi.ss.formula.ptg.ControlPtg;
import org.apache.poi.ss.formula.ptg.FuncVarPtg;
import org.apache.poi.ss.formula.ptg.IntersectionPtg;
import org.apache.poi.ss.formula.ptg.MemAreaPtg;
import org.apache.poi.ss.formula.ptg.MemFuncPtg;
import org.apache.poi.ss.formula.ptg.Ptg;
import org.apache.poi.ss.formula.ptg.RangePtg;
import org.apache.poi.ss.formula.ptg.UnionPtg;
import org.apache.poi.ss.formula.ptg.ValueOperatorPtg;

/**
 * This class performs 'operand class' transformation. Non-base tokens are classified into three
 * operand classes:
 * <ul>
 * <li>reference</li>
 * <li>value</li>
 * <li>array</li>
 * </ul>
 * <p>
 *
 * The final operand class chosen for each token depends on the formula type and the token's place
 * in the formula. If POI gets the operand class wrong, Excel <em>may</em> interpret the formula
 * incorrectly.  This condition is typically manifested as a formula cell that displays as '#VALUE!',
 * but resolves correctly when the user presses F2, enter.<p>
 *
 * The logic implemented here was partially inspired by the description in
 * "OpenOffice.org's Documentation of the Microsoft Excel File Format".  The model presented there
 * seems to be inconsistent with observed Excel behaviour (These differences have not been fully
 * investigated). The implementation in this class has been heavily modified in order to satisfy
 * concrete examples of how Excel performs the same logic (see TestRVA).<p>
 *
 * Hopefully, as additional important test cases are identified and added to the test suite,
 * patterns might become more obvious in this code and allow for simplification.
 */
final class OperandClassTransformer {

    private final FormulaType _formulaType;

    public OperandClassTransformer(FormulaType formulaType) {
        _formulaType = formulaType;
    }

    /**
     * Traverses the supplied formula parse tree, calling {@code Ptg.setClass()} for each non-base
     * token to set its operand class.
     */
    public void transformFormula(ParseNode rootNode) {
        byte rootNodeOperandClass;
        switch (_formulaType) {
            case CELL:
                rootNodeOperandClass = Ptg.CLASS_VALUE;
                break;
            case ARRAY:
                rootNodeOperandClass = Ptg.CLASS_ARRAY;
                break;
            case NAMEDRANGE:
            case DATAVALIDATION_LIST:
                rootNodeOperandClass = Ptg.CLASS_REF;
                break;
            default:
                throw new RuntimeException("Incomplete code - formula type ("
                        + _formulaType + ") not supported yet");

        }
        transformNode(rootNode, rootNodeOperandClass, false);
    }

    /**
     * @param callerForceArrayFlag {@code true} if one of the current node's parents is a
     * function Ptg which has been changed from default 'V' to 'A' type (due to requirements on
     * the function return value).
     */
    private void transformNode(ParseNode node, byte desiredOperandClass,
            boolean callerForceArrayFlag) {
        Ptg token = node.getToken();
        ParseNode[] children = node.getChildren();
        boolean isSimpleValueFunc = isSimpleValueFunction(token);

        if (isSimpleValueFunc) {
            boolean localForceArray = desiredOperandClass == Ptg.CLASS_ARRAY;
            for (ParseNode child : children) {
                transformNode(child, desiredOperandClass, localForceArray);
            }
            setSimpleValueFuncClass((AbstractFunctionPtg) token, desiredOperandClass, callerForceArrayFlag);
            return;
        }

        if (isSingleArgSum(token)) {
            // Need to process the argument of SUM with transformFunctionNode below
            // so make a dummy FuncVarPtg for that call.
            token = FuncVarPtg.SUM;
            // Note - the tAttrSum token (node.getToken()) is a base
            // token so does not need to have its operand class set
        }
        if (token instanceof ValueOperatorPtg || token instanceof ControlPtg
                || token instanceof MemFuncPtg
                || token instanceof MemAreaPtg
                || token instanceof UnionPtg
                || token instanceof IntersectionPtg) {
            // Value Operator Ptgs and Control are base tokens, so token will be unchanged
            // but any child nodes are processed according to desiredOperandClass and callerForceArrayFlag

            // As per OOO documentation Sec 3.2.4 "Token Class Transformation", "Step 1"
            // All direct operands of value operators that are initially 'R' type will
            // be converted to 'V' type.
            byte localDesiredOperandClass = desiredOperandClass == Ptg.CLASS_REF ? Ptg.CLASS_VALUE : desiredOperandClass;
            for (ParseNode child : children) {
                transformNode(child, localDesiredOperandClass, callerForceArrayFlag);
            }
            return;
        }
        if (token instanceof AbstractFunctionPtg) {
            transformFunctionNode((AbstractFunctionPtg) token, children, desiredOperandClass, callerForceArrayFlag);
            return;
        }
        if (children.length > 0) {
            if (token == RangePtg.instance) {
                // TODO is any token transformation required under the various ref operators?
                return;
            }
            throw new IllegalStateException("Node should not have any children");
        }

        if (token.isBaseToken()) {
            // nothing to do
            return;
        }
        token.setClass(transformClass(token.getPtgClass(), desiredOperandClass, callerForceArrayFlag));
    }

    private static boolean isSingleArgSum(Ptg token) {
        if (token instanceof AttrPtg) {
            AttrPtg attrPtg = (AttrPtg) token;
            return attrPtg.isSum();
        }
        return false;
    }

    private static boolean isSimpleValueFunction(Ptg token) {
        if (token instanceof AbstractFunctionPtg) {
            AbstractFunctionPtg aptg = (AbstractFunctionPtg) token;
            if (aptg.getDefaultOperandClass() != Ptg.CLASS_VALUE) {
                return false;
            }
            int numberOfOperands = aptg.getNumberOfOperands();
            for (int i=numberOfOperands-1; i>=0; i--) {
                if (aptg.getParameterClass(i) != Ptg.CLASS_VALUE) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    private byte transformClass(byte currentOperandClass, byte desiredOperandClass,
            boolean callerForceArrayFlag) {
        switch (desiredOperandClass) {
            case Ptg.CLASS_VALUE:
                if (!callerForceArrayFlag) {
                    return Ptg.CLASS_VALUE;
                }
                // else fall through
            case Ptg.CLASS_ARRAY:
                return Ptg.CLASS_ARRAY;
            case Ptg.CLASS_REF:
                if (!callerForceArrayFlag) {
                    return currentOperandClass;
                }
                return Ptg.CLASS_REF;
        }
        throw new IllegalStateException("Unexpected operand class (" + desiredOperandClass + ")");
    }

    private void transformFunctionNode(AbstractFunctionPtg afp, ParseNode[] children,
            byte desiredOperandClass, boolean callerForceArrayFlag) {

        boolean localForceArrayFlag;
        byte defaultReturnOperandClass = afp.getDefaultOperandClass();

        if (callerForceArrayFlag) {
            switch (defaultReturnOperandClass) {
                case Ptg.CLASS_REF:
                    if (desiredOperandClass == Ptg.CLASS_REF) {
                        afp.setClass(Ptg.CLASS_REF);
                    } else {
                        afp.setClass(Ptg.CLASS_ARRAY);
                    }
                    localForceArrayFlag = false;
                    break;
                case Ptg.CLASS_ARRAY:
                    afp.setClass(Ptg.CLASS_ARRAY);
                    localForceArrayFlag = false;
                    break;
                case Ptg.CLASS_VALUE:
                    afp.setClass(Ptg.CLASS_ARRAY);
                    localForceArrayFlag = true;
                    break;
                default:
                    throw new IllegalStateException("Unexpected operand class ("
                            + defaultReturnOperandClass + ")");
            }
        } else {
            if (defaultReturnOperandClass == desiredOperandClass) {
                localForceArrayFlag = false;
                // an alternative would have been to for non-base Ptgs to set their operand class
                // from their default, but this would require the call in many subclasses because
                // the default OC is not known until the end of the constructor
                afp.setClass(defaultReturnOperandClass);
            } else {
                switch (desiredOperandClass) {
                    case Ptg.CLASS_VALUE:
                        // always OK to set functions to return 'value'
                        afp.setClass(Ptg.CLASS_VALUE);
                        localForceArrayFlag = false;
                        break;
                    case Ptg.CLASS_ARRAY:
                        switch (defaultReturnOperandClass) {
                            case Ptg.CLASS_REF:
                                afp.setClass(Ptg.CLASS_REF);
//                              afp.setClass(Ptg.CLASS_ARRAY);
                                break;
                            case Ptg.CLASS_VALUE:
                                afp.setClass(Ptg.CLASS_ARRAY);
                                break;
                            default:
                                throw new IllegalStateException("Unexpected operand class ("
                                        + defaultReturnOperandClass + ")");
                        }
                        localForceArrayFlag = (defaultReturnOperandClass == Ptg.CLASS_VALUE);
                        break;
                    case Ptg.CLASS_REF:
                        switch (defaultReturnOperandClass) {
                            case Ptg.CLASS_ARRAY:
                                afp.setClass(Ptg.CLASS_ARRAY);
                                break;
                            case Ptg.CLASS_VALUE:
                                afp.setClass(Ptg.CLASS_VALUE);
                                break;
                            default:
                                throw new IllegalStateException("Unexpected operand class ("
                                        + defaultReturnOperandClass + ")");
                        }
                        localForceArrayFlag = false;
                        break;
                    default:
                        throw new IllegalStateException("Unexpected operand class ("
                                + desiredOperandClass + ")");
                }

            }
        }

        for (int i = 0; i < children.length; i++) {
            ParseNode child = children[i];
            byte paramOperandClass = afp.getParameterClass(i);
            transformNode(child, paramOperandClass, localForceArrayFlag);
        }
    }

    private void setSimpleValueFuncClass(AbstractFunctionPtg afp,
            byte desiredOperandClass, boolean callerForceArrayFlag) {

        if (callerForceArrayFlag  || desiredOperandClass == Ptg.CLASS_ARRAY) {
            afp.setClass(Ptg.CLASS_ARRAY);
        } else {
            afp.setClass(Ptg.CLASS_VALUE);
        }
    }
}
