/*
// 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) 2006-2015 Pentaho and others
// All Rights Reserved.
 */
package mondrian.olap;

import mondrian.mdx.*;

import org.apache.commons.collections.*;
import org.apache.log4j.Logger;

import java.util.*;

import static org.apache.commons.collections.CollectionUtils.filter;

/**
 * Used to collect and resolve identifiers in groups of children
 * where possible.  For example, if an enumerated set within an MDX
 * query includes references to 10 stores under the parent
 *
 *   [USA].[CA].[San Francisco]
 *
 * the class will attempt to identify those 10 identifiers
 * and issue a single lookup, resulting in fewer and more efficient
 * SQL queries.
 * The resulting collection of resolved identifiers is returned in a
 * map of <QueryPart, QueryPart>, where the unresolved Exp object acts
 * as the key.
 *
 * This class makes no assurances that all identifiers will be resolved.
 * The map returned by .resolve() will include only those identifiers
 * successfully resolved.
 *
 */
public final class IdBatchResolver {
    static final Logger LOGGER = Logger.getLogger(IdBatchResolver.class);

    private final Query query;
    private final Formula[] formulas;
    private final QueryAxis[] axes;
    private final Cube cube;

    // dimension and hierarchy unique names are collected during init
    // to assist in classifying Ids as potentially resolvable to members.
    private final Collection<String> dimensionUniqueNames =
        new ArrayList<String>();
    private final Collection<String> hierarchyUniqueNames =
        new ArrayList<String>();
    // level names are checked against the identifiers to avoid incorrectly
    // interpreting a Dimension.Level reference as Dimension.Member.
    private final Collection<String> levelNames =
        new ArrayList<String>();

    // Set of identifiers, sorted via IdComparator, which orders based
    // first on segment length (shortest to longest), then alphabetically.
    private  SortedSet<Id> identifiers = new TreeSet<Id>(new IdComparator());

    public IdBatchResolver(Query query) {
        this.query = query;
        formulas = query.getFormulas();
        axes = query.getAxes();
        cube = query.getCube();
        initOlapElementNames();
        initIdentifiers();
    }

    /**
     * Initializes the dimensionUniqueNames, hierarchyUniqueNames and
     * levelNames collections based on the contents of cube.  These collections
     * will be used to help determine whether identifiers correspond to
     * a dimension/hierarchy/level.
     */
    private void initOlapElementNames() {
        dimensionUniqueNames.addAll(
            getOlapElementNames(cube.getDimensions(), true));
        for (Dimension dim : cube.getDimensions()) {
            hierarchyUniqueNames.addAll(
                getOlapElementNames(dim.getHierarchies(), true));
            for (Hierarchy hier : dim.getHierarchies()) {
                levelNames.addAll(getOlapElementNames(hier.getLevels(), false));
            }
        }
    }

    /**
     * Initializes the identifiers collection by walking the axes
     * and formulas in the query and adding each encountered Id.
     * Finally, expands the set of identifiers to include parents.  E.g.
     * if the identifier
     *   [Store].[All Store].[USA].[CA]
     * is encountered, this will be expanded to include
     *   [Store].[All Store].[USA]
     *   [Store].[All Store]
     */
    private void initIdentifiers() {
        MdxVisitor identifierVisitor = new IdentifierVisitor(identifiers);
        for (QueryAxis axis : axes) {
            axis.accept(identifierVisitor);
        }
        if (query.getSlicerAxis() != null) {
            query.getSlicerAxis().accept(identifierVisitor);
        }
        for (Formula formula : formulas) {
            formula.accept(identifierVisitor);
        }
        expandIdentifiers(identifiers);
    }

    /**
     * Attempts to resolve the identifiers contained in the query in
     * batches based on the parent, e.g. looking up and resolving the
     * states in the set:
     *   { [Store].[USA].[CA], [Store].[USA].[OR] }
     * together rather than individually.
     * Note that there is no guarantee that all identifiers will be
     * resolved.  Calculated members, for example, are explicitly not
     * handled here.  The purpose of this class is to improve efficiency
     * of resolution of non-calculated members, but must be followed
     * by more thorough expression resolution.
     *
     * @return  a Map of the expressions Id elements mapped to their
     * respective resolved Exp.
     */
    public Map<QueryPart, QueryPart> resolve() {
        return resolveInParentGroupings(identifiers);
    }

    /**
     *  Loops through the SortedSet of Ids, attempting to load sets of
     *  children of parent Ids.
     *  The loop below assumes the the SortedSet is ordered by segment
     *  size from smallest to largest, such that parent identifiers will
     *  occur before their children.
     */
    private  Map<QueryPart, QueryPart> resolveInParentGroupings(
        SortedSet<Id> identifiers)
    {
        final Map<QueryPart, QueryPart> resolvedIdentifiers =
            new HashMap<QueryPart, QueryPart>();

        while (identifiers.size() > 0) {
            Id parent = identifiers.first();
            identifiers.remove(parent);

            if (!supportedIdentifier(parent)) {
                continue;
            }
            Exp exp = (Exp)resolvedIdentifiers.get(parent);
            if (exp == null) {
                exp = lookupExp(resolvedIdentifiers, parent);
            }
            Member parentMember = getMemberFromExp(exp);
            if (!supportedMember(parentMember)) {
                continue;
            }
            batchResolveChildren(
                parent, parentMember, identifiers, resolvedIdentifiers);
        }
        return resolvedIdentifiers;
    }

    /**
     * Find the children of Id parent in the identifiers set and resolves
     * all supported children together, adding them to the resolvedIdentifiers
     * map.
     */
    private void batchResolveChildren(
        Id parent, Member parentMember, SortedSet<Id> identifiers,
        Map<QueryPart, QueryPart> resolvedIdentifiers)
    {
        final List<Id> children = findChildIds(parent, identifiers);
        final List<Id.NameSegment> childNameSegments =
            collectChildrenNameSegments(parentMember, children);

        if (childNameSegments.size() > 0) {
            List<Member> childMembers =
                lookupChildrenByNames(parentMember, childNameSegments);
            addChildrenToResolvedMap(
                resolvedIdentifiers, children, childMembers);
        }
    }

    private Exp lookupExp(
        Map<QueryPart, QueryPart> resolvedIdentifiers, Id parent)
    {
        try {
            Exp exp = Util.lookup(query, parent.getSegments(), false);
            resolvedIdentifiers.put(parent, (QueryPart)exp);
            return exp;
        } catch (Exception exception) {
            LOGGER.info(
                String.format(
                    "Failed to resolve '%s' during batch ID "
                    + "resolution.",
                    parent));
        }
        return null;
    }

    /**
     * Correlates each child Id we started with to it's associated
     * Member, if present.  Updates the resolvedIdentifiers map with
     * the association.
     */
    private void addChildrenToResolvedMap(
        Map<QueryPart, QueryPart> resolvedIdentifiers, List<Id> children,
        List<Member> childMembers)
    {
        for (Member child : childMembers) {
            for (Id childId : children) {
                if (!resolvedIdentifiers.containsKey(childId)
                    && getLastSegment(childId).matches(child.getName()))
                {
                    resolvedIdentifiers.put(
                        childId, (QueryPart)Util.createExpr(child));
                }
            }
        }
    }

    /**
     * Performs a lookup of a set of children under parentMember.
     */
    private List<Member> lookupChildrenByNames(
        Member parentMember,
        List<Id.NameSegment> childNameSegments)
    {
        try {
            return query.getSchemaReader(true)
                .lookupMemberChildrenByNames(
                    parentMember,
                    childNameSegments, MatchType.EXACT);
        } catch (Exception e) {
            LOGGER.info(
                String.format(
                    "Failure while looking up children of '%s' during  "
                    + "batch member resolution.  Child member refs:  %s",
                    parentMember,
                    Arrays.toString(childNameSegments.toArray())), e);
        }
        // don't want to fail at this point.  Member resolution still has
        // another chance to succeed.
        return Collections.emptyList();
    }

    /**
     * Filters the children list to those that contain identifiers
     * we think we can batch resolve, then transforms the Id list
     * to the corresponding NameSegment.
     */
    private List<Id.NameSegment> collectChildrenNameSegments(
        final Member parentMember, List<Id> children)
    {
        filter(
            children, new Predicate() {
            // remove children we can't support
                public boolean evaluate(Object theId)
                {
                    Id id = (Id)theId;
                    return !Util.matches(parentMember, id.getSegments())
                        && supportedIdentifier(id);
                }
            });
        return new ArrayList(
            CollectionUtils.collect(
                children, new Transformer()
            {
                // convert the collection to a list of NameSegments
            public Object transform(Object theId) {
                Id id = (Id)theId;
                return getLastSegment(id);
            }
        }));
    }

    private Id.Segment getLastSegment(Id id) {
        int segSize = id.getSegments().size();
        return id.getSegments().get(segSize - 1);
    }

    /**
     * Checks various conditions to determine whether
     * the given identifier is likely to be resolvable at this point.
     */
    private boolean supportedIdentifier(Id id) {
        Id.Segment seg = getLastSegment(id);
        if (!(seg instanceof Id.NameSegment)) {
            // we can't batch resolve members identified by key
            return false;
        }
        return (isPossibleMemberRef(id))
            && !segmentIsCalcMember(id.getSegments())
            && !id.getSegments().get(0).matches("Measures");
    }

    private boolean supportedMember(Member member) {
        return !(member == null
            || member.equals(
                member.getHierarchy().getNullMember())
            || member.isMeasure());
    }

    /**
     * Returns the [All] member from HierarchyExpr and DimensionExpr
     * associated with hierarchies that have an All member.
     * Returns the member associated with a MemberExpr.
     * For all other Exp returns null.
     */
    private Member getMemberFromExp(Exp exp) {
        if (exp instanceof DimensionExpr) {
            Hierarchy hier = ((DimensionExpr)exp)
                .getDimension().getHierarchy();
            if (hier.hasAll()) {
                return hier.getAllMember();
            }
        } else if (exp instanceof HierarchyExpr) {
            Hierarchy hier = ((HierarchyExpr)exp)
                .getHierarchy();
            if (hier.hasAll()) {
                return hier.getAllMember();
            }
        } else if (exp instanceof MemberExpr) {
            return ((MemberExpr)exp).getMember();
        }
        return null;
    }

    /**
     * Returns a collection of strings corresponding to the name
     * or uniqueName of each OlapElement in olapElements, based on the
     * flag uniqueName.
     */
    private Collection<String> getOlapElementNames(
        OlapElement[] olapElements, final boolean uniqueName)
    {
        return CollectionUtils.collect(
            Arrays.asList(olapElements),
            new Transformer() {
                public Object transform(Object o) {
                    return uniqueName ? ((OlapElement)o).getUniqueName()
                        : ((OlapElement)o).getName();
                }
            });
    }

    /**
     * Returns true if the Id is something that will potentially translate into
     * either the All/Default member of a dimension/hierarchy,
     * or a specific member.
     * This filters out references that we'd be unlikely to effectively
     * handle.
     */
    private boolean isPossibleMemberRef(Id id) {
        int size = id.getSegments().size();

        if (size == 1) {
            //Id.Segment seg = id.getSegments().get(0);
            return segListMatchInUniqueNames(
                id.getSegments(), dimensionUniqueNames)
                || segListMatchInUniqueNames(
                    id.getSegments(), hierarchyUniqueNames);
        }
        if (MondrianProperties.instance().SsasCompatibleNaming.get()
            && size == 2)
        {
            return segListMatchInUniqueNames(
                id.getSegments(), hierarchyUniqueNames);
        }
        if (segMatchInNames(getLastSegment(id), levelNames)) {
            // conservative.  false on any match of any level name
            return false;
        }
        // don't support "shortcut" member references references
        return size > 1;
    }

    private boolean segListMatchInUniqueNames(
        List<Id.Segment> segments, Collection<String> names)
    {
        String segUniqueName = Util.implode(segments);
        for (String name : names) {
           if (Util.equalName(segUniqueName, name)) {
               return true;
           }
        }
        return false;
    }

    private boolean segMatchInNames(
        Id.Segment seg, Collection<String> names)
    {
        for (String name : names) {
            if (seg.matches(name)) {
                return true;
            }
        }
        return false;
    }

    private boolean segmentIsCalcMember(final List<Id.Segment> checkSegments) {
        return query.getSchemaReader(true)
            .getCalculatedMember(checkSegments) != null;
    }

    private List<Id> findChildIds(Id parent, SortedSet<Id> identifiers) {
        List<Id> childIds = new ArrayList<Id>();
        for (Id id : identifiers) {
            final List<Id.Segment> idSeg = id.getSegments();
            final List<Id.Segment> parentSegments = parent.getSegments();
            final int parentSegSize = parentSegments.size();
            if (idSeg.size() == parentSegSize + 1
                && parent.getSegments().equals(
                    idSeg.subList(0, parentSegSize)))
            {
                childIds.add(id);
            }
        }
        return childIds;
    }

    /**
     * Adds each parent segment to the set.
     */
    private void expandIdentifiers(Set<Id> identifiers) {
        Set<Id> expandedIdentifiers = new HashSet<Id>();
        for (Id id : identifiers) {
            for (int i = 1; i < id.getSegments().size(); i++) {
                expandedIdentifiers.add(new Id(id.getSegments().subList(0, i)));
            }
        }
        identifiers.addAll(expandedIdentifiers);
    }

    /**
     * Sorts shorter segments first, then by string compare.
     * This allows processing parents first during the lookup loop,
     * which is required by the algorithm.
     */
    private static class IdComparator implements Comparator<Id> {
        public int compare(Id o1, Id o2) {
            List<Id.Segment> o1Seg = o1.getSegments();
            List<Id.Segment> o2Seg = o2.getSegments();

            if (o1Seg.size() > o2Seg.size()) {
                return 1;
            } else if (o1Seg.size() < o2Seg.size()) {
                return -1;
            } else {
                return o1Seg.toString()
                    .compareTo(o2Seg.toString());
            }
        }
    }
}
// End IdBatchResolver.java
