/*
// This software is subject to the terms of the Eclipse Public License v1.0
// Agreement, available at the following URL:
// http://www.eclipse.org/legal/epl-v10.html.
// You must accept the terms of that agreement to use this software.
//
// Copyright (C) 2005-2005 Julian Hyde
// Copyright (C) 2005-2015 Pentaho
// All Rights Reserved.
*/
package mondrian.olap.fun;

import mondrian.calc.*;
import mondrian.calc.impl.*;
import mondrian.mdx.ResolvedFunCall;
import mondrian.olap.*;
import mondrian.olap.type.*;
import mondrian.rolap.agg.CellRequestQuantumExceededException;
import mondrian.spi.UserDefinedFunction;

import java.util.*;

/**
 * Resolver for user-defined functions.
 *
 * @author jhyde
 * @since 2.0
 */
public class UdfResolver implements Resolver {
    private final UdfFactory factory;
    private final UserDefinedFunction udf;

    private static final String[] emptyStringArray = new String[0];

    public UdfResolver(UdfFactory factory) {
        this.factory = factory;
        this.udf = factory.create();
    }

    public String getName() {
        return udf.getName();
    }

    public String getDescription() {
        return udf.getDescription();
    }

    public String getSignature() {
        Type[] parameterTypes = udf.getParameterTypes();
        int[] parameterCategories = new int[parameterTypes.length];
        for (int i = 0; i < parameterCategories.length; i++) {
            parameterCategories[i] = TypeUtil.typeToCategory(parameterTypes[i]);
        }
        Type returnType = udf.getReturnType(parameterTypes);
        int returnCategory = TypeUtil.typeToCategory(returnType);
        return getSyntax().getSignature(
            getName(),
            returnCategory,
            parameterCategories);
    }

    public Syntax getSyntax() {
        return udf.getSyntax();
    }

    public FunDef getFunDef() {
        Type[] parameterTypes = udf.getParameterTypes();
        int[] parameterCategories = new int[parameterTypes.length];
        for (int i = 0; i < parameterCategories.length; i++) {
            parameterCategories[i] = TypeUtil.typeToCategory(parameterTypes[i]);
        }
        Type returnType = udf.getReturnType(parameterTypes);
        return new UdfFunDef(parameterCategories, returnType);
    }

    public FunDef resolve(
        Exp[] args,
        Validator validator,
        List<Conversion> conversions)
    {
        final Type[] parameterTypes = udf.getParameterTypes();
        if (args.length != parameterTypes.length) {
            return null;
        }
        int[] parameterCategories = new int[parameterTypes.length];
        Type[] castArgTypes = new Type[parameterTypes.length];
        for (int i = 0; i < parameterTypes.length; i++) {
            Type parameterType = parameterTypes[i];
            final Exp arg = args[i];
            final Type argType = arg.getType();
            final int parameterCategory =
                TypeUtil.typeToCategory(parameterType);
            if (!validator.canConvert(
                    i, arg, parameterCategory, conversions))
            {
                return null;
            }
            parameterCategories[i] = parameterCategory;
            if (!parameterType.equals(argType)) {
                castArgTypes[i] =
                    FunDefBase.castType(argType, parameterCategory);
            }
        }
        final Type returnType = udf.getReturnType(castArgTypes);
        return new UdfFunDef(parameterCategories, returnType);
    }

    public boolean requiresExpression(int k) {
        return false;
    }

    public String[] getReservedWords() {
        final String[] reservedWords = udf.getReservedWords();
        return reservedWords == null ? emptyStringArray : reservedWords;
    }

    /**
     * Adapter which converts a {@link UserDefinedFunction} into a
     * {@link FunDef}.
     */
    private class UdfFunDef extends FunDefBase {
        private Type returnType;

        public UdfFunDef(int[] parameterCategories, Type returnType) {
            super(
                UdfResolver.this,
                TypeUtil.typeToCategory(returnType),
                parameterCategories);
            this.returnType = returnType;
        }

        public Type getResultType(Validator validator, Exp[] args) {
            return returnType;
        }

        public Calc compileCall(ResolvedFunCall call, ExpCompiler compiler) {
            final Exp[] args = call.getArgs();
            Calc[] calcs = new Calc[args.length];
            UserDefinedFunction.Argument[] expCalcs =
                new UserDefinedFunction.Argument[args.length];
            for (int i = 0; i < args.length; i++) {
                Exp arg = args[i];
                final Calc calc = calcs[i] = compiler.compileAs(
                    arg,
                    castType(arg.getType(), parameterCategories[i]),
                    ResultStyle.ANY_LIST);
                calcs[i] = calc;
                final Calc scalarCalc = compiler.compileScalar(arg, true);
                final ListCalc listCalc;
                final IterCalc iterCalc;
                if (arg.getType() instanceof SetType) {
                    listCalc = compiler.compileList(arg, true);
                    iterCalc = compiler.compileIter(arg);
                } else {
                    listCalc = null;
                    iterCalc = null;
                }
                expCalcs[i] = new CalcExp(calc, scalarCalc, listCalc, iterCalc);
            }

            // Create a new instance of the UDF, because some UDFs use member
            // variables as state.
            UserDefinedFunction udf2 = factory.create();
            if (call.getType() instanceof SetType) {
                return new ListCalcImpl(call, calcs, udf2, expCalcs);
            } else {
                return new ScalarCalcImpl(call, calcs, udf2, expCalcs);
            }
        }
    }

    /**
     * Expression that evaluates a scalar user-defined function.
     */
    private static class ScalarCalcImpl extends GenericCalc {
        private final Calc[] calcs;
        private final UserDefinedFunction udf;
        private final UserDefinedFunction.Argument[] args;

        public ScalarCalcImpl(
            ResolvedFunCall call,
            Calc[] calcs,
            UserDefinedFunction udf,
            UserDefinedFunction.Argument[] args)
        {
            super(call);
            this.calcs = calcs;
            this.udf = udf;
            this.args = args;
        }

        public Calc[] getCalcs() {
            return calcs;
        }

        public Object evaluate(Evaluator evaluator) {
            try {
                return udf.execute(evaluator, args);
            } catch (CellRequestQuantumExceededException e) {
                // Is assumed will be processed in mondrian.rolap.RolapResult
                throw e;
            } catch (Exception e) {
                return FunUtil.newEvalException(
                    "Exception while executing function " + udf.getName(),
                    e);
            }
        }

        public boolean dependsOn(Hierarchy hierarchy) {
            // Be pessimistic. This effectively disables expression caching.
            return true;
        }
    }

    /**
     * Expression that evaluates a list user-defined function.
     */
    private static class ListCalcImpl extends AbstractListCalc {
        private final UserDefinedFunction udf;
        private final UserDefinedFunction.Argument[] args;

        public ListCalcImpl(
            ResolvedFunCall call,
            Calc[] calcs,
            UserDefinedFunction udf,
            UserDefinedFunction.Argument[] args)
        {
            super(call, calcs);
            this.udf = udf;
            this.args = args;
        }

        public TupleList evaluateList(Evaluator evaluator) {
            final List list = (List) udf.execute(evaluator, args);

            // If arity is 1, assume they have returned a list of members.
            // For other arity, assume a list of member arrays.
            if (getType().getArity() == 1) {
                //noinspection unchecked
                return new UnaryTupleList((List<Member>) list);
            } else {
                // Use an adapter to make a list of member arrays look like
                // a list of members laid end-to-end.
                final int arity = getType().getArity();
                //noinspection unchecked
                final List<Member[]> memberArrayList = (List<Member[]>) list;
                return new ListTupleList(
                    arity,
                    new AbstractList<Member>() {
                        @Override
                        public Member get(int index) {
                            return memberArrayList.get(index / arity)
                                [index % arity];
                        }

                        @Override
                        public int size() {
                            return memberArrayList.size() * arity;
                        }
                    }
                );
            }
        }

        @Override
        public boolean dependsOn(Hierarchy hierarchy) {
            // Be pessimistic. This effectively disables expression caching.
            return true;
        }
    }

    /**
     * Wrapper around a {@link Calc} to make it appear as an {@link Exp}.
     * Only the {@link #evaluate(mondrian.olap.Evaluator)}
     * and {@link #evaluateScalar(mondrian.olap.Evaluator)} methods are
     * supported.
     */
    private static class CalcExp implements UserDefinedFunction.Argument {
        private final Calc calc;
        private final Calc scalarCalc;
        private final IterCalc iterCalc;
        private final ListCalc listCalc;

        /**
         * Creates a CalcExp.
         *
         * @param calc Compiled expression
         * @param scalarCalc Compiled expression that evaluates to a scalar
         * @param listCalc Compiled expression that evaluates an MDX set to
         *     a java list
         * @param iterCalc Compiled expression that evaluates an MDX set to
         */
        public CalcExp(
            Calc calc,
            Calc scalarCalc,
            ListCalc listCalc,
            IterCalc iterCalc)
        {
            this.calc = calc;
            this.scalarCalc = scalarCalc;
            this.listCalc = listCalc;
            this.iterCalc = iterCalc;
        }

        public Type getType() {
            return calc.getType();
        }

        public Object evaluate(Evaluator evaluator) {
            return adapt(calc.evaluate(evaluator));
        }

        public Object evaluateScalar(Evaluator evaluator) {
            return scalarCalc.evaluate(evaluator);
        }

        public List evaluateList(Evaluator eval) {
            if (listCalc == null) {
                throw new RuntimeException("Expression is not a set");
            }
            return adaptList(listCalc.evaluateList(eval));
        }

        public Iterable evaluateIterable(Evaluator eval) {
            if (iterCalc == null) {
                throw new RuntimeException("Expression is not a set");
            }
            return adaptIterable(iterCalc.evaluateIterable(eval));
        }

        /**
         * Adapts the output of {@link TupleList} and {@link TupleIterable}
         * calculator expressions to the old style, that returned either members
         * or arrays of members.
         *
         * @param o Output of calc
         * @return Output in new format (lists and iterables over lists of
         *    members)
         */
        private Object adapt(Object o) {
            if (o instanceof TupleIterable) {
                return adaptIterable((TupleIterable) o);
            }
            return o;
        }

        private List adaptList(final TupleList tupleList) {
            // List is required to be mutable -- so make a copy.
            if (tupleList.getArity() == 1) {
                return new ArrayList<Member>(tupleList.slice(0));
            } else {
                return new ArrayList<Member[]>(
                    TupleCollections.asMemberArrayList(tupleList));
            }
        }

        private Iterable adaptIterable(final TupleIterable tupleIterable) {
            if (tupleIterable instanceof TupleList) {
                return adaptList((TupleList) tupleIterable);
            }
            if (tupleIterable.getArity() == 1) {
                return tupleIterable.slice(0);
            } else {
                return TupleCollections.asMemberArrayIterable(tupleIterable);
            }
        }
    }

    /**
     * Factory for {@link UserDefinedFunction}.
     *
     * <p>This factory is required because a user-defined function is allowed
     * to store state in itself. Therefore it is unsanitary to use the same
     * UDF in the function table for validation and runtime. In the function
     * table there is a factory. We use one instance of instance of the UDF to
     * validate, and create another for the runtime plan.</p>
     */
    public interface UdfFactory {
        /**
         * Creates a UDF.
         *
         * @return UDF
         */
        UserDefinedFunction create();
    }

    /**
     * Implementation of {@link UdfFactory} that instantiates a given class
     * using a public default constructor.
     */
    public static class ClassUdfFactory implements UdfResolver.UdfFactory {
        private final Class<? extends UserDefinedFunction> clazz;
        private final String name;

        /**
         * Creates a ClassUdfFactory.
         *
         * @param clazz Class to instantiate
         * @param name Name
         */
        public ClassUdfFactory(
            Class<? extends UserDefinedFunction> clazz,
            String name)
        {
            this.clazz = clazz;
            this.name = name;
            assert clazz != null;
        }

        public UserDefinedFunction create() {
            return Util.createUdf(clazz, name);
        }
    }
}

// End UdfResolver.java
