/*
// Licensed to Julian Hyde under one or more contributor license
// agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership.
//
// Julian Hyde 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.olap4j.query;

import org.olap4j.Axis;
import org.olap4j.mdx.*;
import org.olap4j.metadata.Level;
import org.olap4j.metadata.Member;

import java.util.*;

/**
 * Utility class to convert a Query object to a SelectNode.
 */
abstract class Olap4jNodeConverter {

    public static SelectNode toOlap4j(Query query) {
        List<IdentifierNode> list = Collections.emptyList();
        List<ParseTreeNode> withList;
        if (query.getWithList().isEmpty()) {
            withList = Collections.emptyList();
        } else {
            withList = query.getWithList();
        }
        List<QueryAxis> axisList = new ArrayList<QueryAxis>();
        axisList.add(query.getAxes().get(Axis.COLUMNS));
        axisList.add(query.getAxes().get(Axis.ROWS));

        AxisNode filterAxis = null;
        if (query.getAxes().containsKey(Axis.FILTER)) {
            final QueryAxis axis = query.getAxes().get(Axis.FILTER);
            if (!axis.dimensions.isEmpty()) {
                filterAxis = toOlap4j(axis);
            }
        }
        return new SelectNode(
            null,
            withList,
            toOlap4j(axisList),
            new CubeNode(
                null,
                query.getCube()),
            filterAxis,
            list);
    }

    private static CallNode generateSetCall(ParseTreeNode... args) {
        return
            new CallNode(
                null,
                "{}",
                Syntax.Braces,
                args);
    }

    private static CallNode generateListSetCall(List<ParseTreeNode> cnodes) {
        return
            new CallNode(
                null,
                "{}",
                Syntax.Braces,
                cnodes);
    }

    private static CallNode generateListTupleCall(List<ParseTreeNode> cnodes) {
        return
            new CallNode(
                null,
                "()",
                Syntax.Parentheses,
                cnodes);
    }

    protected static CallNode generateCrossJoin(List<ParseTreeNode> selections)
    {
        ParseTreeNode sel1 = selections.remove(0);
        if (sel1 instanceof MemberNode) {
            sel1 = generateSetCall(sel1);
        }
        if (selections.size() == 1) {
            ParseTreeNode sel2 = selections.get(0);
            if (sel2 instanceof MemberNode) {
                sel2 = generateSetCall(sel2);
            }
            return new CallNode(
                null, "CrossJoin", Syntax.Function, sel1, sel2);
        } else {
            return new CallNode(
                null, "CrossJoin", Syntax.Function, sel1,
                generateCrossJoin(selections));
        }
    }

    protected static CallNode generateUnion(List<List<ParseTreeNode>> unions) {
        if (unions.size() > 2) {
            List<ParseTreeNode> first = unions.remove(0);
            return new CallNode(
                null, "Union", Syntax.Function,
                generateCrossJoin(first),
                generateUnion(unions));
        } else {
            return new CallNode(
                null, "Union", Syntax.Function,
                generateCrossJoin(unions.get(0)),
                generateCrossJoin(unions.get(1)));
        }
    }

    protected static CallNode generateHierarchizeUnion(
        List<List<ParseTreeNode>> unions)
    {
        return new CallNode(
            null, "Hierarchize", Syntax.Function,
            generateUnion(unions));
    }

    /**
     * Algorithm:<ul>
     * <li>generate all combinations of dimension groups
     * <li>skip the selection if has a context
     * <li>for all the selections with context, resolve them last
     * <li>union all combinations
     * </ul>
     */
    private static void generateUnionsRecursively(
        QueryAxis axis,
        int dim,
        List<ParseTreeNode> curr,
        List<List<ParseTreeNode>> unions,
        List<Selection> selsWithContext,
        List<List<ParseTreeNode>> contextUnions)
    {
        ParseTreeNode exceptSet = null;
        QueryDimension qDim = axis.getDimensions().get(dim);

        List<Selection> exclusionSelections = qDim.getExclusions();
        List<ParseTreeNode> exclusionNodes = new ArrayList<ParseTreeNode>();

        // Check if any exclusions are selected for this dimension
        // and convert them to a list of nodes and then later a set
        for (Selection exclusion : exclusionSelections) {
            exclusionNodes.add(toOlap4j(exclusion));
        }
        if (exclusionNodes.size() > 0) {
            exceptSet = generateListSetCall(exclusionNodes);
        }

        for (Selection sel : qDim.getInclusions()) {
            ParseTreeNode selectionNode = toOlap4j(sel);
            // If a the querydimension should return only hierarchy
            // consistent results, generate a filter that checks
            // inclusions for ancestors in higher levels
            if (qDim.isHierarchyConsistent()
                    && qDim.getInclusions().size() > 1)
            {
                Integer currentDepth = null;
                if (sel.getRootElement() instanceof Member) {
                    currentDepth = ((Member)sel.getRootElement()).getDepth();
                } else if (sel.getRootElement() instanceof Level) {
                    currentDepth = ((Level)sel.getRootElement()).getDepth();
                }
                selectionNode =
                   toHierarchyConsistentNode(selectionNode, currentDepth, qDim);
            }
            // If a sort Order was specified for this dimension
            // apply it for this inclusion
            if (qDim.getSortOrder() != null) {
                CallNode currentMemberNode =
                    new CallNode(
                        null,
                        "CurrentMember",
                        Syntax.Property,
                        new DimensionNode(null, sel.getDimension()));
                CallNode currentMemberNameNode =
                    new CallNode(
                        null,
                        "Name",
                        Syntax.Property,
                        currentMemberNode);
                selectionNode =
                    new CallNode(
                        null,
                        "Order",
                        Syntax.Function,
                        generateSetCall(selectionNode),
                        currentMemberNameNode,
                        LiteralNode.createSymbol(
                            null,
                            qDim.getSortOrder().name()));
            }
            // If there are exlclusions wrap the ordered selection
            // in an Except() function
            if (exceptSet != null) {
                selectionNode =
                    new CallNode(
                        null,
                        "Except",
                        Syntax.Function,
                        generateSetCall(selectionNode),
                        exceptSet);
            }
            if (sel.getSelectionContext() != null
                && sel.getSelectionContext().size() > 0)
            {
                // selections that have a context are treated
                // differently than the rest of the MDX generation
                if (!selsWithContext.contains(sel)) {
                    ArrayList<ParseTreeNode> sels =
                        new ArrayList<ParseTreeNode>();
                    for (int i = 0; i < axis.getDimensions().size(); i++) {
                        if (dim == i) {
                            sels.add(selectionNode);
                        } else {
                            // return the selections in the correct
                            // dimensional order
                            QueryDimension dimension =
                                axis.getDimensions().get(i);

                            boolean found = false;
                            for (Selection selection
                                : sel.getSelectionContext())
                            {
                                if (selection.getDimension().equals(
                                        dimension.getDimension()))
                                {
                                    sels.add(toOlap4j(selection));
                                    found = true;
                                }
                            }
                            if (!found) {
                                // add the first selection of the dimension
                                if (dimension.getInclusions().size() > 0) {
                                    sels.add(toOlap4j(
                                        dimension.getInclusions().get(0)));
                                }
                            }
                        }
                    }
                    contextUnions.add(sels);
                    selsWithContext.add(sel);
                }
            } else {
                List<ParseTreeNode> ncurr = new ArrayList<ParseTreeNode>();
                if (curr != null) {
                    ncurr.addAll(curr);
                }
                ncurr.add(selectionNode);
                if (dim == axis.getDimensions().size() - 1) {
                    // last dimension
                    unions.add(ncurr);
                } else {
                    generateUnionsRecursively(
                        axis, dim + 1, ncurr, unions, selsWithContext,
                        contextUnions);
                }
            }
        }
    }

    /**
     * Merges the selections into a single
     * MDX axis selection.  Right now we do a simple
     * crossjoin.
     * It might return null if there are no dimensions placed on the axis.
     */
    private static AxisNode toOlap4j(QueryAxis axis) {
        CallNode callNode = null;
        int numDimensions = axis.getDimensions().size();
        if (numDimensions == 0) {
            return null;
        } else if (numDimensions == 1) {
            QueryDimension dimension = axis.getDimensions().get(0);
            List<ParseTreeNode> members = toOlap4j(dimension);
            callNode = generateListSetCall(members);
        } else {
            // generate union sets of selections in each dimension
            List<List<ParseTreeNode>> unions =
                new ArrayList<List<ParseTreeNode>>();
            List<Selection> selsWithContext = new ArrayList<Selection>();
            List<List<ParseTreeNode>> contextUnions =
                new ArrayList<List<ParseTreeNode>>();
            generateUnionsRecursively(
                axis, 0, null, unions, selsWithContext, contextUnions);
            unions.addAll(contextUnions);
            if (unions.size() > 1) {
                if (axis.getHierarchizeMode() != null
                    && axis.getHierarchizeMode()
                        .equals(QueryDimension.HierarchizeMode.POST))
                {
                    callNode = generateHierarchizeUnionPost(unions);
                } else {
                    callNode = generateHierarchizeUnion(unions);
                }
            } else {
                callNode = generateCrossJoin(unions.get(0));
            }
        }

        ParseTreeNode filteredNode = null;
        if (axis.getFilterCondition() != null) {
            LiteralNode conditionNode =
                LiteralNode.createSymbol(
                    null,
                    axis.getFilterCondition());
            filteredNode =
                new CallNode(
                    null,
                    "Filter",
                    Syntax.Function,
                    callNode,
                    conditionNode);
        } else {
            filteredNode = callNode;
        }
        // We might need to limit the axis set
        ParseTreeNode limitedNode = null;
        if (axis.getLimitFunction() != null) {
            ParseTreeNode n =
                LiteralNode.createNumeric(
                    null,
                    axis.getLimitFunctionN(),
                    false);
            if (axis.getLimitFunctionSortLiteral() != null) {
                LiteralNode evaluatorNode = null;
                evaluatorNode =
                    LiteralNode.createSymbol(
                        null,
                        axis.getLimitFunctionSortLiteral());
                limitedNode =
                    new CallNode(
                        null,
                        axis.getLimitFunction().toString(),
                        Syntax.Function,
                        filteredNode,
                        n,
                        evaluatorNode);
            } else {
                limitedNode =
                    new CallNode(
                        null,
                        axis.getLimitFunction().toString(),
                        Syntax.Function,
                        filteredNode,
                        n);
            }
        } else {
            limitedNode = filteredNode;
        }
        // We might need to sort the whole axis.
        ParseTreeNode sortedNode = null;
        if (axis.getSortOrder() != null) {
            LiteralNode evaluatorNode =
                 LiteralNode.createSymbol(
                     null,
                     axis.getSortIdentifierNodeName());
            sortedNode =
                new CallNode(
                    null,
                    "Order",
                    Syntax.Function,
                    limitedNode,
                    evaluatorNode,
                    LiteralNode.createSymbol(
                        null, axis.getSortOrder().name()));
        } else {
            sortedNode = limitedNode;
        }

        return new AxisNode(
            null,
            axis.isNonEmpty(),
            axis.getLocation(),
            new ArrayList<IdentifierNode>(),
            sortedNode);
    }

    protected static CallNode generateHierarchizeUnionPost(
        List<List<ParseTreeNode>> unions)
    {
        return new CallNode(
            null, "Hierarchize", Syntax.Function,
            generateUnion(unions),
            LiteralNode.createSymbol(
                null, QueryDimension.HierarchizeMode.POST.name()));
    }

    private static List<ParseTreeNode> toOlap4j(QueryDimension dimension) {
        // Let's build a first list of included members.
        List<ParseTreeNode> includeList = new ArrayList<ParseTreeNode>();
        Map<Integer, List<ParseTreeNode>> levelNodes =
            new HashMap<Integer, List<ParseTreeNode>>();
        for (Selection selection : dimension.getInclusions()) {
            ParseTreeNode selectionNode = toOlap4j(selection);
            // If a the querydimension should return only hierarchy
            // consistent results, generate a filter that checks
            // inclusions for ancestors in higher levels
            if (dimension.isHierarchyConsistent()
                && dimension.getInclusions().size() > 1)
            {
                Integer curdepth = 0;
                if (selection.getRootElement() instanceof Member) {
                    curdepth = ((Member)selection.getRootElement()).getDepth();
                } else if (selection.getRootElement() instanceof Level) {
                    curdepth = ((Level)selection.getRootElement()).getDepth();
                }

                if (levelNodes.get(curdepth) != null) {
                    levelNodes.get(curdepth).add(selectionNode);
                } else {
                    List<ParseTreeNode> nodes = new ArrayList<ParseTreeNode>();
                    nodes.add(selectionNode);
                    levelNodes.put(curdepth, nodes);
                }
            } else {
                includeList.add(selectionNode);
            }
        }
        if (dimension.isHierarchyConsistent()
            && dimension.getInclusions().size() > 1)
        {
            Integer levelDepths[] =
                levelNodes.keySet()
                    .toArray(new Integer[levelNodes.keySet().size()]);

            Arrays.sort(levelDepths);

            for (Integer depth : levelDepths) {
                ParseTreeNode levelNode =
                    generateListSetCall(levelNodes.get(depth));

                levelNode =
                    toHierarchyConsistentNode(levelNode, depth, dimension);
                includeList.add(levelNode);
            }
        }
        // If a sort order was specified, we need to wrap the inclusions in an
        // Order() mdx function.
        List<ParseTreeNode> orderedList = new ArrayList<ParseTreeNode>();
        if (dimension.getSortOrder() != null) {
            CallNode currentMemberNode = new CallNode(
                null,
                "CurrentMember",
                Syntax.Property,
                new DimensionNode(null, dimension.getDimension()));
            CallNode currentMemberNameNode = new CallNode(
                null,
                "Name",
                Syntax.Property,
                currentMemberNode);
            orderedList.add(
                new CallNode(
                    null,
                    "Order",
                    Syntax.Function,
                    generateListSetCall(includeList),
                    currentMemberNameNode,
                    LiteralNode.createSymbol(
                        null, dimension.getSortOrder().name())));
        } else {
            orderedList.addAll(includeList);
        }

        // We're not done yet. We might have to exclude members from the
        // inclusion, so we might have to wrap the list in a mdx Except()
        // function call.
        List<ParseTreeNode> listWithExclusions =
            new ArrayList<ParseTreeNode>();
        if (dimension.getExclusions().size() > 0) {
            List<ParseTreeNode> excludedMembers =
                new ArrayList<ParseTreeNode>();
            for (Selection selection : dimension.getExclusions()) {
                excludedMembers.add(toOlap4j(selection));
            }
            listWithExclusions.add(
                new CallNode(
                    null,
                    "Except",
                    Syntax.Function,
                    generateListSetCall(orderedList),
                    generateListSetCall(excludedMembers)));
        } else {
            listWithExclusions.addAll(orderedList);
        }

        // Do we need to wrap it all in a Hierarchize function?
        List<ParseTreeNode> listWithHierarchy =
            new ArrayList<ParseTreeNode>();
        if (dimension.getHierarchizeMode() != null) {
            CallNode hierarchyNode;
            // There are two modes available, PRE and POST.
            if (dimension.getHierarchizeMode().equals(
                    QueryDimension.HierarchizeMode.PRE))
            {
                // In pre mode, we don't add the "POST" literal.
                hierarchyNode = new CallNode(
                    null,
                    "Hierarchize",
                    Syntax.Function,
                    generateListSetCall(listWithExclusions));
            } else if (dimension.getHierarchizeMode().equals(
                    QueryDimension.HierarchizeMode.POST))
            {
                hierarchyNode = new CallNode(
                    null,
                    "Hierarchize",
                    Syntax.Function,
                    generateListSetCall(listWithExclusions),
                    LiteralNode.createSymbol(
                        null, dimension.getHierarchizeMode().name()));
            } else {
                throw new RuntimeException("Missing value handler.");
            }
            listWithHierarchy.add(hierarchyNode);
        } else {
            listWithHierarchy.addAll(listWithExclusions);
        }

        return listWithHierarchy;
    }

    private static ParseTreeNode toOlap4j(Selection selection) {
        try {
            return selection.visit();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    static ParseTreeNode toOlap4j(
        Member member,
        Selection.Operator oper)
    {
        ParseTreeNode node = null;
        try {
            switch (oper) {
            case MEMBER:
                node = new MemberNode(null, member);
                break;
            case SIBLINGS:
                node =
                    new CallNode(
                        null,
                        "Siblings",
                        Syntax.Property,
                        new MemberNode(null, member));
                break;
            case CHILDREN:
                node =
                    new CallNode(
                        null,
                        "Children",
                        Syntax.Property,
                        new MemberNode(null, member));
                break;
            case INCLUDE_CHILDREN:
                node =
                    generateSetCall(
                        new MemberNode(null, member),
                        toOlap4j(member, Selection.Operator.CHILDREN));
                break;
            case DESCENDANTS:
                node =
                    new CallNode(
                        null,
                        "Descendants",
                        Syntax.Function,
                        new MemberNode(null, member));
                break;
            case ANCESTORS:
                node =
                    new CallNode(
                        null,
                        "Ascendants",
                        Syntax.Function,
                        new MemberNode(null, member));
                break;
            default:
                System.out.println("NOT IMPLEMENTED: " + oper);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return node;
    }

    static ParseTreeNode toOlap4j(
        Level level,
        Selection.Operator oper)
    {
        ParseTreeNode node = null;
        try {
            switch (oper) {
            case MEMBERS:
                node =
                    new CallNode(
                        null,
                        "Members",
                        Syntax.Property,
                        new LevelNode(null, level));
                break;
            default:
                System.out.println("NOT IMPLEMENTED: " + oper);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return node;
    }

    private static List<AxisNode> toOlap4j(List<QueryAxis> axes) {
        final ArrayList<AxisNode> axisList = new ArrayList<AxisNode>();
        for (QueryAxis axis : axes) {
            AxisNode axisNode = toOlap4j(axis);
            if (axisNode != null) {
                axisList.add(toOlap4j(axis));
            }
        }
        return axisList;
    }

    private static ParseTreeNode toHierarchyConsistentNode(
        ParseTreeNode selectionNode,
        Integer maxDepth,
        QueryDimension qDim)
    {
        // If a the querydimension should return only hierarchy
        // consistent results, generate a filter that checks
        // inclusions for ancestors in higher levels
        if (qDim.getInclusions().size() > 1) {
            Map<Integer, Level> levels = new HashMap<Integer, Level>();
            for (Selection s : qDim.getInclusions()) {
                if (s.getRootElement() instanceof Member) {
                    Integer d = ((Member)s.getRootElement()).getDepth();
                    if (!levels.containsKey(d)) {
                        Level lvl = ((Member)s.getRootElement()).getLevel();
                        levels.put(d, lvl);
                    }
                } else if (s.getRootElement() instanceof Level) {
                    Integer d = ((Level)s.getRootElement()).getDepth();
                    if (!levels.containsKey(d)) {
                        Level lvl = ((Level)s.getRootElement());
                        levels.put(d, lvl);
                    }
                }
            }
            Integer levelDepths[] =
                levels.keySet()
                    .toArray(new Integer[levels.keySet().size()]);

            Arrays.sort(levelDepths);

            List<CallNode> inConditions = new ArrayList<CallNode>();
            for (Integer i = 0; i < levelDepths.length - 1; i++) {
                Level currentLevel = levels.get(levelDepths[i]);
                if (levelDepths[i] < maxDepth
                    && currentLevel.getLevelType() != Level.Type.ALL)
                {
                    CallNode currentMemberNode =
                        new CallNode(
                            null,
                            "CurrentMember",
                            Syntax.Property,
                            new HierarchyNode(
                                null,
                                currentLevel.getHierarchy()));

                    CallNode ancestorNode =
                        new CallNode(
                            null,
                            "Ancestor",
                            Syntax.Function,
                            currentMemberNode,
                            new LevelNode(null, currentLevel));

                    List <ParseTreeNode> ancestorList =
                        new ArrayList<ParseTreeNode>();

                    for (Selection anc : qDim.getInclusions()) {
                        if (anc.getRootElement() instanceof Member) {
                            Level l = ((Member)anc.getRootElement()).getLevel();
                            if (l.equals(levels.get(levelDepths[i]))) {
                                ancestorList.add(anc.visit());
                            }
                        }
                    }
                    if (ancestorList.size() > 0) {
                        CallNode ancestorSet =
                            generateListSetCall(ancestorList);
                        CallNode inClause = new CallNode(
                            null,
                            "Exists",
                            Syntax.Function,
                            ancestorNode,
                            ancestorSet);
                        CallNode count =
                            new CallNode(
                                null,
                                "Count",
                                Syntax.Property,
                                inClause);
                        CallNode greaterZero =
                            new CallNode(
                                null,
                                " > 0",
                                Syntax.Postfix,
                                count);
                        inConditions.add(greaterZero);
                    }
                }
            }
            if (inConditions.size() > 0) {
                CallNode chainedIn = inConditions.get(0);
                if (inConditions.size() > 1) {
                    for (int c = 1;c < inConditions.size();c++) {
                        chainedIn = new CallNode(
                            null,
                            "AND",
                            Syntax.Infix,
                            chainedIn,
                            inConditions.get(c));
                    }
                }

                return new CallNode(
                    null,
                    "Filter",
                    Syntax.Function,
                    generateSetCall(selectionNode),
                    chainedIn);
            }
        }
        return selectionNode;
    }
}

// End Olap4jNodeConverter.java










