/*
// 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) 2003-2005 Julian Hyde
// Copyright (C) 2005-2015 Pentaho
// All Rights Reserved.
*/
package mondrian.rolap;

import mondrian.calc.*;
import mondrian.calc.impl.AbstractCalc;
import mondrian.calc.impl.GenericCalc;
import mondrian.olap.*;
import mondrian.olap.type.StringType;
import mondrian.rolap.sql.MemberChildrenConstraint;
import mondrian.rolap.sql.TupleConstraint;

import org.apache.log4j.Logger;

import org.eigenbase.util.property.Property;

import org.olap4j.mdx.IdentifierSegment;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import javax.sql.DataSource;

/**
 * A <code>RolapSchemaReader</code> allows you to read schema objects while
 * observing the access-control profile specified by a given role.
 *
 * @author jhyde
 * @since Feb 24, 2003
 */
public class RolapSchemaReader
    implements SchemaReader,
        RolapNativeSet.SchemaReaderWithMemberReaderAvailable,
        NameResolver.Namespace
{
    protected final Role role;
    private final Map<Hierarchy, MemberReader> hierarchyReaders =
        new ConcurrentHashMap<Hierarchy, MemberReader>();
    protected final RolapSchema schema;
    private final SqlConstraintFactory sqlConstraintFactory =
        SqlConstraintFactory.instance();
    private static final Logger LOGGER =
        Logger.getLogger(RolapSchemaReader.class);

    /**
     * Creates a RolapSchemaReader.
     *
     * @param role Role for access control, must not be null
     * @param schema Schema
     */
    RolapSchemaReader(Role role, RolapSchema schema) {
        assert role != null : "precondition: role != null";
        assert schema != null;
        this.role = role;
        this.schema = schema;
    }

    public Role getRole() {
        return role;
    }

    public List<Member> getHierarchyRootMembers(Hierarchy hierarchy) {
        final Role.HierarchyAccess hierarchyAccess =
            role.getAccessDetails(hierarchy);
        final Level[] levels = hierarchy.getLevels();
        final Level firstLevel;
        if (hierarchyAccess == null) {
            firstLevel = levels[0];
        } else {
            firstLevel = levels[hierarchyAccess.getTopLevelDepth()];
        }
        return getLevelMembers(firstLevel, true);
    }

    /**
     * This method uses a double-checked locking idiom to avoid making the
     * method fully synchronized, or potentially creating the same MemberReader
     * more than once.  Double-checked locking can cause issues if
     * a second thread accesses the field without either a shared lock in
     * place or the field being specified as volatile.
     * In this case, hierarchyReaders is a ConcurrentHashMap,
     * which internally uses volatile load semantics for read operations.
     * This assures values written by one thread will be visible when read by
     * others.
     * http://en.wikipedia.org/wiki/Double-checked_locking
     */
    public MemberReader getMemberReader(Hierarchy hierarchy) {
        MemberReader memberReader = hierarchyReaders.get(hierarchy);
        if (memberReader == null) {
            synchronized (this) {
                memberReader = hierarchyReaders.get(hierarchy);
                if (memberReader == null) {
                    memberReader =
                        ((RolapHierarchy) hierarchy).createMemberReader(role);
                    hierarchyReaders.put(hierarchy, memberReader);
                }
            }
        }
        return memberReader;
    }


    public Member substitute(Member member) {
        final MemberReader memberReader =
            getMemberReader(member.getHierarchy());
        return memberReader.substitute((RolapMember) member);
    }

    public void getMemberRange(
        Level level, Member startMember, Member endMember, List<Member> list)
    {
        getMemberReader(level.getHierarchy()).getMemberRange(
            (RolapLevel) level, (RolapMember) startMember,
            (RolapMember) endMember, Util.<RolapMember>cast(list));
    }

    public int compareMembersHierarchically(Member m1, Member m2) {
        RolapMember member1 = (RolapMember) m1;
        RolapMember member2 = (RolapMember) m2;
        final RolapHierarchy hierarchy = member1.getHierarchy();
        Util.assertPrecondition(hierarchy == m2.getHierarchy());
        return getMemberReader(hierarchy).compare(member1, member2, true);
    }

    public Member getMemberParent(Member member) {
        return getMemberReader(member.getHierarchy()).getMemberParent(
            (RolapMember) member);
    }

    public int getMemberDepth(Member member) {
        final Role.HierarchyAccess hierarchyAccess =
            role.getAccessDetails(member.getHierarchy());
        if (hierarchyAccess != null) {
            final int memberDepth = member.getLevel().getDepth();
            final int topLevelDepth = hierarchyAccess.getTopLevelDepth();
            return memberDepth - topLevelDepth;
        } else if (((RolapLevel) member.getLevel()).isParentChild()) {
            // For members of parent-child hierarchy, members in the same level
            // may have different depths.
            int depth = 0;
            for (Member m = member.getParentMember();
                m != null;
                m = m.getParentMember())
            {
                depth++;
            }
            return depth;
        } else {
            return member.getLevel().getDepth();
        }
    }


    public List<Member> getMemberChildren(Member member) {
        return getMemberChildren(member, null);
    }

    public List<Member> getMemberChildren(Member member, Evaluator context) {
        MemberChildrenConstraint constraint =
            sqlConstraintFactory.getMemberChildrenConstraint(context);
        List<RolapMember> memberList =
            internalGetMemberChildren(member, constraint);
        return Util.cast(memberList);
    }

    /**
     * Helper for getMemberChildren.
     *
     * @param member Member
     * @param constraint Constraint
     * @return List of children
     */
    private List<RolapMember> internalGetMemberChildren(
        Member member, MemberChildrenConstraint constraint)
    {
        List<RolapMember> children = new ArrayList<RolapMember>();
        final Hierarchy hierarchy = member.getHierarchy();
        final MemberReader memberReader = getMemberReader(hierarchy);
        memberReader.getMemberChildren(
            (RolapMember) member, children, constraint);
        return children;
    }

    public void getParentChildContributingChildren(
        Member dataMember,
        Hierarchy hierarchy,
        List<Member> list)
    {
        final List<RolapMember> rolapMemberList = Util.cast(list);
        list.add(dataMember);
        ((RolapHierarchy) hierarchy).getMemberReader().getMemberChildren(
            (RolapMember) dataMember, rolapMemberList);
    }

    public int getChildrenCountFromCache(Member member) {
        final Hierarchy hierarchy = member.getHierarchy();
        final MemberReader memberReader = getMemberReader(hierarchy);
        if (memberReader instanceof
            RolapCubeHierarchy.RolapCubeHierarchyMemberReader)
        {
            List list =
                ((RolapCubeHierarchy.RolapCubeHierarchyMemberReader)
                 memberReader)
                    .getRolapCubeMemberCacheHelper()
                    .getChildrenFromCache((RolapMember) member, null);
            if (list == null) {
                return -1;
            }
            return list.size();
        }

        if (memberReader instanceof SmartMemberReader) {
            List list = ((SmartMemberReader) memberReader).getMemberCache()
                .getChildrenFromCache((RolapMember) member, null);
            if (list == null) {
                return -1;
            }
            return list.size();
        }
        if (!(memberReader instanceof MemberCache)) {
            return -1;
        }
        List list = ((MemberCache) memberReader)
            .getChildrenFromCache((RolapMember) member, null);
        if (list == null) {
            return -1;
        }
        return list.size();
    }

    /**
     * Returns number of members in a level,
     * if the information can be retrieved from cache.
     * Otherwise {@link Integer#MIN_VALUE}.
     *
     * @param level Level
     * @return number of members in level
     */
    private int getLevelCardinalityFromCache(Level level) {
        final Hierarchy hierarchy = level.getHierarchy();
        final MemberReader memberReader = getMemberReader(hierarchy);
        if (memberReader instanceof
            RolapCubeHierarchy.RolapCubeHierarchyMemberReader)
        {
            final MemberCacheHelper cache =
                ((RolapCubeHierarchy.RolapCubeHierarchyMemberReader)
                    memberReader).getRolapCubeMemberCacheHelper();
            if (cache == null) {
                return Integer.MIN_VALUE;
            }
            final List<RolapMember> list =
                cache.getLevelMembersFromCache(
                    (RolapLevel) level, null);
            if (list == null) {
                return Integer.MIN_VALUE;
            }
            return list.size();
        }

        if (memberReader instanceof SmartMemberReader) {
            List<RolapMember> list =
                ((SmartMemberReader) memberReader)
                    .getMemberCache()
                    .getLevelMembersFromCache(
                        (RolapLevel) level, null);
            if (list == null) {
                return Integer.MIN_VALUE;
            }
            return list.size();
        }

        if (memberReader instanceof MemberCache) {
            List<RolapMember> list =
                ((MemberCache) memberReader)
                    .getLevelMembersFromCache(
                        (RolapLevel) level, null);
            if (list == null) {
                return Integer.MIN_VALUE;
            }
            return list.size();
        }

        return Integer.MIN_VALUE;
    }

    public int getLevelCardinality(
        Level level,
        boolean approximate,
        boolean materialize)
    {
        if (!this.role.canAccess(level)) {
            return 1;
        }

        int rowCount = Integer.MIN_VALUE;
        if (approximate) {
            // See if the schema has an approximation.
            rowCount = level.getApproxRowCount();
        }

        if (rowCount == Integer.MIN_VALUE) {
            // See if the precise row count is available in cache.
            rowCount = getLevelCardinalityFromCache(level);
        }

        if (rowCount == Integer.MIN_VALUE) {
            if (materialize) {
                // Either the approximate row count hasn't been set,
                // or they want the precise row count.
                final MemberReader memberReader =
                    getMemberReader(level.getHierarchy());
                rowCount =
                    memberReader.getLevelMemberCount((RolapLevel) level);
                // Cache it for future.
                ((RolapLevel) level).setApproxRowCount(rowCount);
            }
        }
        return rowCount;
    }

    public List<Member> getMemberChildren(List<Member> members) {
        return getMemberChildren(members, null);
    }

    public List<Member> getMemberChildren(
        List<Member> members,
        Evaluator context)
    {
        if (members.size() == 0) {
            return Collections.emptyList();
        } else {
            MemberChildrenConstraint constraint =
                sqlConstraintFactory.getMemberChildrenConstraint(context);
            final Hierarchy hierarchy = members.get(0).getHierarchy();
            final MemberReader memberReader = getMemberReader(hierarchy);
            final List<RolapMember> rolapMemberList = Util.cast(members);
            final List<RolapMember> children = new ArrayList<RolapMember>();
            memberReader.getMemberChildren(
                rolapMemberList,
                children,
                constraint);
            return Util.cast(children);
        }
    }

    public void getMemberAncestors(Member member, List<Member> ancestorList) {
        Member parentMember = getMemberParent(member);
        while (parentMember != null) {
            ancestorList.add(parentMember);
            parentMember = getMemberParent(parentMember);
        }
    }

    public Cube getCube() {
        throw new UnsupportedOperationException();
    }

    public SchemaReader withoutAccessControl() {
        assert this.getClass() == RolapSchemaReader.class
            : "Subclass " + getClass() + " must override";
        if (role == schema.getDefaultRole()) {
            return this;
        }
        return new RolapSchemaReader(schema.getDefaultRole(), schema);
    }

    public OlapElement getElementChild(OlapElement parent, Id.Segment name) {
        return getElementChild(parent, name, MatchType.EXACT);
    }

    public OlapElement getElementChild(
        OlapElement parent, Id.Segment name, MatchType matchType)
    {
        return parent.lookupChild(this, name, matchType);
    }

    public final Member getMemberByUniqueName(
        List<Id.Segment> uniqueNameParts,
        boolean failIfNotFound)
    {
        return getMemberByUniqueName(
            uniqueNameParts, failIfNotFound, MatchType.EXACT);
    }

    public Member getMemberByUniqueName(
        List<Id.Segment> uniqueNameParts,
        boolean failIfNotFound,
        MatchType matchType)
    {
        // In general, this schema reader doesn't have a cube, so we cannot
        // start looking up members.
        return null;
    }

    public OlapElement lookupCompound(
        OlapElement parent,
        List<Id.Segment> names,
        boolean failIfNotFound,
        int category)
    {
        return lookupCompound(
            parent, names, failIfNotFound, category, MatchType.EXACT);
    }

    public final OlapElement lookupCompound(
        OlapElement parent,
        List<Id.Segment> names,
        boolean failIfNotFound,
        int category,
        MatchType matchType)
    {
        if (MondrianProperties.instance().SsasCompatibleNaming.get()) {
            return new NameResolver().resolve(
                parent,
                Util.toOlap4j(names),
                failIfNotFound,
                category,
                matchType,
                getNamespaces());
        }
        return lookupCompoundInternal(
            parent,
            names,
            failIfNotFound,
            category,
            matchType);
    }

    public final OlapElement lookupCompoundInternal(
        OlapElement parent,
        List<Id.Segment> names,
        boolean failIfNotFound,
        int category,
        MatchType matchType)
    {
        return Util.lookupCompound(
            this, parent, names, failIfNotFound, category, matchType);
    }

    public List<NameResolver.Namespace> getNamespaces() {
        return Collections.<NameResolver.Namespace>singletonList(this);
    }

    public OlapElement lookupChild(
        OlapElement parent,
        IdentifierSegment segment)
    {
        return lookupChild(parent, segment, MatchType.EXACT);
    }

    public OlapElement lookupChild(
        OlapElement parent,
        IdentifierSegment segment,
        MatchType matchType)
    {
        OlapElement element = getElementChild(
            parent,
            Util.convert(segment),
            matchType);
        if (element != null) {
            return element;
        }
        if (parent instanceof Cube) {
            // Named sets defined at the schema level do not, of course, belong
            // to a cube. But if parent is a cube, this indicates that the name
            // has not been qualified.
            element = schema.getNamedSet(segment);
        }
        return element;
    }

    public Member lookupMemberChildByName(
        Member parent, Id.Segment childName, MatchType matchType)
    {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(
                "looking for child \"" + childName + "\" of " + parent);
        }
        assert !(parent instanceof RolapHierarchy.LimitedRollupMember);
        try {
            MemberChildrenConstraint constraint;
            if (childName instanceof Id.NameSegment
                && matchType.isExact())
            {
                constraint = sqlConstraintFactory.getChildByNameConstraint(
                    (RolapMember) parent, (Id.NameSegment) childName);
            } else {
                constraint =
                    sqlConstraintFactory.getMemberChildrenConstraint(null);
            }
            List<RolapMember> children =
                internalGetMemberChildren(parent, constraint);
            if (children.size() > 0) {
                return
                    RolapUtil.findBestMemberMatch(
                        children,
                        (RolapMember) parent,
                        children.get(0).getLevel(),
                        childName,
                        matchType);
            }
        } catch (NumberFormatException e) {
            // this was thrown in SqlQuery#quote(boolean numeric, Object
            // value). This happens when Mondrian searches for unqualified Olap
            // Elements like [Month], because it tries to look up a member with
            // that name in all dimensions. Then it generates for example
            // "select .. from time where year = Month" which will result in a
            // NFE because "Month" can not be parsed as a number. The real bug
            // is probably, that Mondrian looks at members at all.
            //
            // @see RolapCube#lookupChild()
            LOGGER.debug(
                "NumberFormatException in lookupMemberChildByName "
                + "for parent = \"" + parent
                + "\", childName=\"" + childName
                + "\", exception: " + e.getMessage());
        }
        return null;
    }

    public List<Member> lookupMemberChildrenByNames(
        Member parent, List<Id.NameSegment> childNames, MatchType matchType)
    {
        MemberChildrenConstraint constraint = sqlConstraintFactory
            .getChildrenByNamesConstraint(
                (RolapMember) parent, childNames);
        List<RolapMember> children =
            internalGetMemberChildren(parent, constraint);
        List<Member> childMembers = new ArrayList<Member>();
        childMembers.addAll(children);
        return childMembers;
    }

    public Member getCalculatedMember(List<Id.Segment> nameParts) {
        // There are no calculated members defined against a schema.
        return null;
    }

    public NamedSet getNamedSet(List<Id.Segment> nameParts) {
        if (nameParts.size() != 1) {
            return null;
        }
        if (!(nameParts.get(0) instanceof Id.NameSegment)) {
            return null;
        }
        final String name = ((Id.NameSegment) nameParts.get(0)).name;
        return schema.getNamedSet(name);
    }

    public Member getLeadMember(Member member, int n) {
        final MemberReader memberReader =
            getMemberReader(member.getHierarchy());
        return memberReader.getLeadMember((RolapMember) member, n);
    }

    public List<Member> getLevelMembers(Level level, boolean includeCalculated)
    {
        List<Member> members = getLevelMembers(level, null);
        if (!includeCalculated) {
            members = SqlConstraintUtils.removeCalculatedMembers(members);
        }
        return members;
    }

    public List<Member> getLevelMembers(Level level, Evaluator context) {
        TupleConstraint constraint =
            sqlConstraintFactory.getLevelMembersConstraint(
                context,
                new Level[] {level});
        final MemberReader memberReader =
            getMemberReader(level.getHierarchy());
        List<RolapMember> membersInLevel =
            memberReader.getMembersInLevel(
                (RolapLevel) level, constraint);
        return Util.cast(membersInLevel);
    }

    public List<Dimension> getCubeDimensions(Cube cube) {
        assert cube != null;
        final List<Dimension> dimensions = new ArrayList<Dimension>();
        for (Dimension dimension : cube.getDimensions()) {
            switch (role.getAccess(dimension)) {
            case NONE:
                continue;
            default:
                dimensions.add(dimension);
                break;
            }
        }
        return dimensions;
    }

    public List<Hierarchy> getDimensionHierarchies(Dimension dimension) {
        assert dimension != null;
        final List<Hierarchy> hierarchies = new ArrayList<Hierarchy>();
        for (Hierarchy hierarchy : dimension.getHierarchies()) {
            switch (role.getAccess(hierarchy)) {
            case NONE:
                continue;
            default:
                hierarchies.add(hierarchy);
                break;
            }
        }
        return hierarchies;
    }

    public List<Level> getHierarchyLevels(Hierarchy hierarchy) {
        assert hierarchy != null;
        final Role.HierarchyAccess hierarchyAccess =
            role.getAccessDetails(hierarchy);
        final Level[] levels = hierarchy.getLevels();
        if (hierarchyAccess == null) {
            return Arrays.asList(levels);
        }
        Level topLevel = levels[hierarchyAccess.getTopLevelDepth()];
        Level bottomLevel = levels[hierarchyAccess.getBottomLevelDepth()];
        List<Level> restrictedLevels =
            Arrays.asList(levels).subList(
                topLevel.getDepth(), bottomLevel.getDepth() + 1);
        assert restrictedLevels.size() >= 1 : "postcondition";
        return restrictedLevels;
    }

    public Member getHierarchyDefaultMember(Hierarchy hierarchy) {
        assert hierarchy != null;
        // If the whole hierarchy is inaccessible, return the intrinsic default
        // member. This is important to construct a evaluator.
        if (role.getAccess(hierarchy) == Access.NONE) {
            return hierarchy.getDefaultMember();
        }
        return getMemberReader(hierarchy).getDefaultMember();
    }

    public boolean isDrillable(Member member) {
        final RolapLevel level = (RolapLevel) member.getLevel();
        if (level.getParentExp() != null) {
            // This is a parent-child level, so its children, if any, come from
            // the same level.
            //
            // todo: More efficient implementation
            return getMemberChildren(member).size() > 0;
        } else {
            // This is a regular level. It has children iff there is a lower
            // level.
            final Level childLevel = level.getChildLevel();
            return (childLevel != null)
                && (role.getAccess(childLevel) != Access.NONE);
        }
    }

    public boolean isVisible(Member member) {
        return !member.isHidden() && role.canAccess(member);
    }

    public Cube[] getCubes() {
        List<RolapCube> cubes = schema.getCubeList();
        List<Cube> visibleCubes = new ArrayList<Cube>(cubes.size());

        for (Cube cube : cubes) {
            if (role.canAccess(cube)) {
                visibleCubes.add(cube);
            }
        }

        return visibleCubes.toArray(new Cube[visibleCubes.size()]);
    }

    public List<Member> getCalculatedMembers(Hierarchy hierarchy) {
        return Collections.emptyList();
    }

    public List<Member> getCalculatedMembers(Level level) {
        return Collections.emptyList();
    }

    public List<Member> getCalculatedMembers() {
        return Collections.emptyList();
    }

    public NativeEvaluator getNativeSetEvaluator(
        FunDef fun, Exp[] args, Evaluator evaluator, Calc calc)
    {
        RolapEvaluator revaluator = (RolapEvaluator)
            AbstractCalc.simplifyEvaluator(calc, evaluator);
        if (evaluator.nativeEnabled()) {
            return schema.getNativeRegistry().createEvaluator(
                revaluator, fun, args);
        }
        return null;
    }

    public Parameter getParameter(String name) {
        // Scan through schema parameters.
        for (RolapSchemaParameter parameter : schema.parameterList) {
            if (Util.equalName(parameter.getName(), name)) {
                return parameter;
            }
        }

        // Scan through mondrian properties.
        List<Property> propertyList =
            MondrianProperties.instance().getPropertyList();
        for (Property property : propertyList) {
            if (property.getPath().equals(name)) {
                return new SystemPropertyParameter(name, false);
            }
        }

        return null;
    }

    public DataSource getDataSource() {
        return schema.getInternalConnection().getDataSource();
    }

    public RolapSchema getSchema() {
        return schema;
    }

    public SchemaReader withLocus() {
        return RolapUtil.locusSchemaReader(
            schema.getInternalConnection(),
            this);
    }

    public Map<? extends Member, Access> getMemberChildrenWithDetails(
        Member member,
        Evaluator evaluator)
    {
        MemberChildrenConstraint constraint =
            sqlConstraintFactory.getMemberChildrenConstraint(evaluator);
        final Hierarchy hierarchy = member.getHierarchy();
        final MemberReader memberReader = getMemberReader(hierarchy);
        final ArrayList<RolapMember> memberChildren =
            new ArrayList<RolapMember>();

        return memberReader.getMemberChildren(
            (RolapMember) member,
            memberChildren,
            constraint);
    }

    /**
     * Implementation of {@link Parameter} which is sourced from mondrian
     * properties (see {@link MondrianProperties}.
     * <p/>
     * <p>The name of the property is the same as the key into the
     * {@link java.util.Properties} object; for example "mondrian.trace.level".
     */
    private static class SystemPropertyParameter
        extends ParameterImpl
    {
        /**
         * true if source is a system property;
         * false if source is a mondrian property.
         */
        private final boolean system;
        /**
         * Definition of mondrian property, or null if system property.
         */
        private final Property propertyDefinition;

        public SystemPropertyParameter(String name, boolean system) {
            super(
                name,
                Literal.nullValue,
                "System property '" + name + "'",
                new StringType());
            this.system = system;
            this.propertyDefinition =
                system
                ? null
                : MondrianProperties.instance().getPropertyDefinition(name);
        }

        public Scope getScope() {
            return Scope.System;
        }

        public boolean isModifiable() {
            return false;
        }

        public Calc compile(ExpCompiler compiler) {
            return new GenericCalc(new DummyExp(getType())) {
                public Calc[] getCalcs() {
                    return new Calc[0];
                }

                public Object evaluate(Evaluator evaluator) {
                    if (system) {
                        final String name =
                            SystemPropertyParameter.this.getName();
                        return System.getProperty(name);
                    } else {
                        return propertyDefinition.stringValue();
                    }
                }
            };
        }
    }
}

// End RolapSchemaReader.java
