/*
// 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.driver.xmla;

import org.olap4j.OlapException;
import org.olap4j.driver.xmla.XmlaOlap4jConnection.BackendFlavor;
import org.olap4j.impl.*;
import org.olap4j.mdx.IdentifierSegment;
import org.olap4j.metadata.*;

import java.lang.ref.SoftReference;
import java.util.*;

/**
 * Implementation of {@link Cube}
 * for XML/A providers.
 *
 * @author jhyde
 * @since Dec 4, 2007
 */
class XmlaOlap4jCube implements Cube, Named
{
    final XmlaOlap4jSchema olap4jSchema;
    private final String name;
    private final String caption;
    private final String description;

    final NamedList<XmlaOlap4jDimension> dimensions;
    final Map<String, XmlaOlap4jDimension> dimensionsByUname =
        new HashMap<String, XmlaOlap4jDimension>();
    private NamedList<XmlaOlap4jHierarchy> hierarchies = null;
    final Map<String, XmlaOlap4jHierarchy> hierarchiesByUname =
        new HashMap<String, XmlaOlap4jHierarchy>();
    final Map<String, XmlaOlap4jLevel> levelsByUname =
        new HashMap<String, XmlaOlap4jLevel>();
    final List<XmlaOlap4jMeasure> measures =
        new ArrayList<XmlaOlap4jMeasure>();
    private final NamedList<XmlaOlap4jNamedSet> namedSets;
    private final MetadataReader metadataReader;

    /**
     * Creates an XmlaOlap4jCube.
     *
     * @param olap4jSchema Schema
     * @param name Name
     * @param caption Caption
     * @param description Description
     * @throws org.olap4j.OlapException on error
     */
    XmlaOlap4jCube(
        XmlaOlap4jSchema olap4jSchema,
        String name,
        String caption,
        String description) throws OlapException
    {
        assert olap4jSchema != null;
        assert description != null;
        assert name != null;
        this.olap4jSchema = olap4jSchema;
        this.name = name;
        this.caption = caption;
        this.description = description;
        final Map<String, XmlaOlap4jMeasure> measuresMap =
            new HashMap<String, XmlaOlap4jMeasure>();
        this.metadataReader =
            new CachingMetadataReader(
                new RawMetadataReader(),
                measuresMap);
        // In case this is the dummy cube for shared dimensions stop here
        // to avoid unnecessary calls and errors with unique members
        if ("".equals(name)) {
            namedSets = null;
            dimensions = null;
            return;
        }

        final XmlaOlap4jConnection olap4jConnection =
            olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData.olap4jConnection;

        final XmlaOlap4jConnection.Context context =
            new XmlaOlap4jConnection.Context(this, null, null, null);

        String[] restrictions = {
            "CATALOG_NAME", olap4jSchema.olap4jCatalog.getName(),
            "SCHEMA_NAME", olap4jSchema.getName(),
            "CUBE_NAME", getName()
        };

        this.dimensions = new DeferredNamedListImpl<XmlaOlap4jDimension>(
            XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_DIMENSIONS,
            context,
            new XmlaOlap4jConnection.DimensionHandler(this),
            restrictions);

        // populate measures up front; a measure is needed in every query
        olap4jConnection.populateList(
            measures,
            context,
            XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_MEASURES,
            new XmlaOlap4jConnection.MeasureHandler(),
            restrictions);
        for (XmlaOlap4jMeasure measure : measures) {
            measuresMap.put(measure.getUniqueName(), measure);
        }

        // populate named sets
        namedSets = new DeferredNamedListImpl<XmlaOlap4jNamedSet>(
            XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_SETS,
            context,
            new XmlaOlap4jConnection.NamedSetHandler(),
            restrictions);
    }

    public Schema getSchema() {
        return olap4jSchema;
    }

    public String getName() {
        return name;
    }

    public String getUniqueName() {
        return "[" + name + "]";
    }

    public String getCaption() {
        return caption;
    }

    public String getDescription() {
        return description;
    }

    public boolean isVisible() {
        return true;
    }

    public NamedList<Dimension> getDimensions() {
        return Olap4jUtil.cast(dimensions);
    }

    public NamedList<Hierarchy> getHierarchies() {
        // This is a costly operation. It forces the init
        // of all dimensions and all hierarchies.
        // We defer it to this point.
        if (this.hierarchies == null) {
            this.hierarchies = new NamedListImpl<XmlaOlap4jHierarchy>();
            for (XmlaOlap4jDimension dim : this.dimensions) {
                this.hierarchies.addAll(dim.hierarchies);
            }
        }
        return Olap4jUtil.cast(hierarchies);
    }

    public boolean isDrillThroughEnabled() {
        // XMLA does not implement drillthrough yet.
        return false;
    }

    public List<Measure> getMeasures() {
        return Olap4jUtil.cast(measures);
    }

    public NamedList<NamedSet> getSets() {
        return Olap4jUtil.cast(namedSets);
    }

    public Collection<Locale> getSupportedLocales() {
        return Collections.singletonList(Locale.getDefault());
    }

    public Member lookupMember(
        List<IdentifierSegment> segmentList)
        throws OlapException
    {
        StringBuilder buf = new StringBuilder();
        for (IdentifierSegment segment : segmentList) {
            if (buf.length() > 0) {
                buf.append('.');
            }
            buf.append(segment.toString());
        }
        final String uniqueName = buf.toString();
        return getMetadataReader().lookupMemberByUniqueName(uniqueName);
    }

    /**
     * Returns this cube's metadata reader.
     *
     * <p>Not part of public olap4j API.
     *
     * @return metadata reader
     */
    MetadataReader getMetadataReader() {
        return metadataReader;
    }

    public List<Member> lookupMembers(
        Set<Member.TreeOp> treeOps,
        List<IdentifierSegment> nameParts) throws OlapException
    {
        StringBuilder buf = new StringBuilder();
        for (IdentifierSegment namePart : nameParts) {
            if (buf.length() > 0) {
                buf.append('.');
            }
            buf.append(namePart);
        }
        final String uniqueName = buf.toString();
        final List<XmlaOlap4jMember> list =
            new ArrayList<XmlaOlap4jMember>();
        getMetadataReader().lookupMemberRelatives(
            treeOps, uniqueName, list);
        return Olap4jUtil.cast(list);
    }

    /**
     * Abstract implementation of MemberReader that delegates all operations
     * to an underlying MemberReader.
     */
    private static abstract class DelegatingMetadataReader
        implements MetadataReader
    {
        private final MetadataReader metadataReader;

        /**
         * Creates a DelegatingMetadataReader.
         *
         * @param metadataReader Underlying metadata reader
         */
        DelegatingMetadataReader(MetadataReader metadataReader) {
            this.metadataReader = metadataReader;
        }

        public XmlaOlap4jMember lookupMemberByUniqueName(
            String memberUniqueName) throws OlapException
        {
            return metadataReader.lookupMemberByUniqueName(memberUniqueName);
        }

        public void lookupMembersByUniqueName(
            List<String> memberUniqueNames,
            Map<String, XmlaOlap4jMember> memberMap) throws OlapException
        {
            metadataReader.lookupMembersByUniqueName(
                memberUniqueNames, memberMap);
        }

        public void lookupMemberRelatives(
            Set<Member.TreeOp> treeOps,
            String memberUniqueName,
            List<XmlaOlap4jMember> list) throws OlapException
        {
            metadataReader.lookupMemberRelatives(
                treeOps, memberUniqueName, list);
        }

        public List<XmlaOlap4jMember> getLevelMembers(
            XmlaOlap4jLevel level)
            throws OlapException
        {
            return metadataReader.getLevelMembers(level);
        }
    }

    /**
     * Implementation of MemberReader that reads from an underlying member
     * reader and caches the results.
     *
     * <p>Caches are {@link Map}s containing
     * {@link java.lang.ref.SoftReference}s to cached objects, so can be
     * cleared when memory is in short supply.
     */
    private static class CachingMetadataReader
        extends DelegatingMetadataReader
    {
        private final Map<String, XmlaOlap4jMeasure> measuresMap;

        private final Map<String, SoftReference<XmlaOlap4jMember>> memberMap =
            new HashMap<String, SoftReference<XmlaOlap4jMember>>();

        private final Map<
            XmlaOlap4jLevel,
            SoftReference<List<XmlaOlap4jMember>>> levelMemberListMap =
            new HashMap<
                XmlaOlap4jLevel,
                SoftReference<List<XmlaOlap4jMember>>>();

        /**
         * Creates a CachingMetadataReader.
         *
         * @param metadataReader Underlying metadata reader
         * @param measuresMap Map of measures by unique name, inherited from the
         *     cube and used read-only by this reader
         */
        CachingMetadataReader(
            MetadataReader metadataReader,
            Map<String, XmlaOlap4jMeasure> measuresMap)
        {
            super(metadataReader);
            this.measuresMap = measuresMap;
        }

        public XmlaOlap4jMember lookupMemberByUniqueName(
            String memberUniqueName) throws OlapException
        {
            // First, look in measures map.
            XmlaOlap4jMeasure measure =
                measuresMap.get(memberUniqueName);
            if (measure != null) {
                return measure;
            }

            // Next, look in cache.
            final SoftReference<XmlaOlap4jMember> memberRef =
                memberMap.get(memberUniqueName);
            if (memberRef != null) {
                final XmlaOlap4jMember member = memberRef.get();
                if (member != null) {
                    return member;
                }
            }

            final XmlaOlap4jMember member =
                super.lookupMemberByUniqueName(memberUniqueName);
            if (member != null
                && member.getDimension().type != Dimension.Type.MEASURE)
            {
                memberMap.put(
                    memberUniqueName,
                    new SoftReference<XmlaOlap4jMember>(member));
            }
            return member;
        }

        public void lookupMembersByUniqueName(
            List<String> memberUniqueNames,
            Map<String, XmlaOlap4jMember> memberMap) throws OlapException
        {
            final ArrayList<String> remainingMemberUniqueNames =
                new ArrayList<String>();
            for (String memberUniqueName : memberUniqueNames) {
                // First, look in measures map.
                XmlaOlap4jMeasure measure =
                    measuresMap.get(memberUniqueName);
                if (measure != null) {
                    memberMap.put(memberUniqueName, measure);
                    continue;
                }

                // Next, look in cache.
                final SoftReference<XmlaOlap4jMember> memberRef =
                    this.memberMap.get(memberUniqueName);
                final XmlaOlap4jMember member;
                if (memberRef != null
                    && (member = memberRef.get()) != null)
                {
                    memberMap.put(memberUniqueName, member);
                    continue;
                }

                remainingMemberUniqueNames.add(memberUniqueName);
            }

            // If any of the member names were not in the cache, look them up
            // by delegating.
            if (!remainingMemberUniqueNames.isEmpty()) {
                super.lookupMembersByUniqueName(
                    remainingMemberUniqueNames,
                    memberMap);
                // Add the previously missing members into the cache.
                for (String memberName : remainingMemberUniqueNames) {
                    XmlaOlap4jMember member = memberMap.get(memberName);
                    if (member != null) {
                        if (!(member instanceof Measure)
                            && member.getDimension().type
                               != Dimension.Type.MEASURE)
                        {
                            this.memberMap.put(
                                memberName,
                                new SoftReference<XmlaOlap4jMember>(member));
                        }
                    }
                }
            }
        }

        public List<XmlaOlap4jMember> getLevelMembers(
            XmlaOlap4jLevel level)
            throws OlapException
        {
            final SoftReference<List<XmlaOlap4jMember>> memberListRef =
                levelMemberListMap.get(level);
            if (memberListRef != null) {
                final List<XmlaOlap4jMember> memberList = memberListRef.get();
                if (memberList != null) {
                    return memberList;
                }
            }
            final List<XmlaOlap4jMember> memberList =
                super.getLevelMembers(level);
            if (level.olap4jHierarchy.olap4jDimension.type
                != Dimension.Type.MEASURE)
            {
                levelMemberListMap.put(
                    level,
                    new SoftReference<List<XmlaOlap4jMember>>(memberList));
            }
            return memberList;
        }
    }

    /**
     * Implementation of MetadataReader that reads from the XMLA provider,
     * without caching.
     */
    private class RawMetadataReader implements MetadataReader {
        public XmlaOlap4jMember lookupMemberByUniqueName(
            String memberUniqueName)
            throws OlapException
        {
            NamedList<XmlaOlap4jMember> list =
                new NamedListImpl<XmlaOlap4jMember>();
            lookupMemberRelatives(
                Olap4jUtil.enumSetOf(Member.TreeOp.SELF),
                memberUniqueName,
                list);
            switch (list.size()) {
            case 0:
                return null;
            case 1:
                return list.get(0);
            default:
                String providerName =
                    olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData
                        .olap4jConnection.getOlapDatabase().getProviderName();
                if ("Microsoft Analysis Services".equals(providerName)) {
                    return list.get(0);
                }
                throw new IllegalArgumentException(
                    "more than one member with unique name '"
                    + memberUniqueName
                    + "'");
            }
        }

        public void lookupMembersByUniqueName(
            List<String> memberUniqueNames,
            Map<String, XmlaOlap4jMember> memberMap) throws OlapException
        {
            final XmlaOlap4jDatabaseMetaData m =
                olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData;
            switch (BackendFlavor.getFlavor(m.olap4jConnection, false)) {
            case MONDRIAN:
                mondrianMembersLookup(memberUniqueNames, memberMap);
                break;
            default:
                genericMembersLookup(memberUniqueNames, memberMap);
            }
        }

        /**
         * Looks up members; optimized for Mondrian servers.
         *
         * @param memberUniqueNames A list of the members to lookup
         * @param memberMap Output map of members keyed by unique name
         * @throws OlapException Gets thrown for communication errors
         */
        private void mondrianMembersLookup(
            List<String> memberUniqueNames,
            Map<String, XmlaOlap4jMember> memberMap) throws OlapException
        {
            final XmlaOlap4jConnection.Context context =
                new XmlaOlap4jConnection.Context(
                    XmlaOlap4jCube.this, null, null, null);
            final List<XmlaOlap4jMember> memberList =
                new ArrayList<XmlaOlap4jMember>();
            olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData.olap4jConnection
                .populateList(
                    memberList,
                    context,
                    XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_MEMBERS,
                    new XmlaOlap4jConnection.MemberHandler(),
                    new Object[] {
                        "CATALOG_NAME", olap4jSchema.olap4jCatalog.getName(),
                        "SCHEMA_NAME", olap4jSchema.getName(),
                        "CUBE_NAME", getName(),
                        "MEMBER_UNIQUE_NAME", memberUniqueNames
                    });
            for (XmlaOlap4jMember member : memberList) {
                if (member != null) {
                    memberMap.put(member.getUniqueName(), member);
                }
            }
        }

        /**
         * Looks up members.
         *
         * @param memberUniqueNames A list of the members to lookup
         * @param memberMap Output map of members keyed by unique name
         * @throws OlapException Gets thrown for communication errors
         */
        private void genericMembersLookup(
            List<String> memberUniqueNames,
            Map<String, XmlaOlap4jMember> memberMap) throws OlapException
        {
            // Iterates through member names
            for (String currentMemberName : memberUniqueNames) {
                // Only lookup if it is not in the map yet
                if (!memberMap.containsKey(currentMemberName)) {
                    XmlaOlap4jMember member =
                        this.lookupMemberByUniqueName(currentMemberName);
                    // Null members might mean calculated members
                    if (member != null) {
                        memberMap.put(member.getUniqueName(), member);
                    }
                }
            }
        }

        public void lookupMemberRelatives(
            Set<Member.TreeOp> treeOps,
            String memberUniqueName,
            List<XmlaOlap4jMember> list) throws OlapException
        {
            final XmlaOlap4jConnection.Context context =
                new XmlaOlap4jConnection.Context(
                    XmlaOlap4jCube.this, null, null, null);
            int treeOpMask = 0;
            for (Member.TreeOp treeOp : treeOps) {
                treeOpMask |= treeOp.xmlaOrdinal();
            }
            olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData.olap4jConnection
                .populateList(
                    list,
                    context,
                    XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_MEMBERS,
                    new XmlaOlap4jConnection.MemberHandler(),
                    new Object[] {
                        "CATALOG_NAME",
                        olap4jSchema.olap4jCatalog.getName(),
                        "SCHEMA_NAME",
                        olap4jSchema.getName(),
                        "CUBE_NAME", getName(),
                        "MEMBER_UNIQUE_NAME",
                        memberUniqueName,
                        "TREE_OP",
                        String.valueOf(treeOpMask)
                    });
        }

        public List<XmlaOlap4jMember> getLevelMembers(
            XmlaOlap4jLevel level)
            throws OlapException
        {
            assert level.olap4jHierarchy.olap4jDimension.olap4jCube
                == XmlaOlap4jCube.this;
            final XmlaOlap4jConnection.Context context =
                new XmlaOlap4jConnection.Context(level);
            List<XmlaOlap4jMember> list = new ArrayList<XmlaOlap4jMember>();
            // If this is a level in the [Measures] dimension, we want to
            // return objects that implement the Measure interface. During
            // bootstrap, the list will be empty, and we need to return the
            // regular Member objects which have the extra properties that are
            // returned by MSCHEMA_MEMBERS but not MDSCHEMA_MEASURES.
            switch (level.getDimension().getDimensionType()) {
            case MEASURE:
                if (!level.olap4jHierarchy.olap4jDimension.olap4jCube.measures
                    .isEmpty())
                {
                    return Olap4jUtil.cast(
                        level.olap4jHierarchy.olap4jDimension.olap4jCube
                            .measures);
                }
                break;
            }
            olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData.olap4jConnection
                .populateList(
                    list,
                    context,
                    XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_MEMBERS,
                    new XmlaOlap4jConnection.MemberHandler(),
                    new Object[] {
                        "CATALOG_NAME", olap4jSchema.olap4jCatalog.getName(),
                        "SCHEMA_NAME", olap4jSchema.getName(),
                        "CUBE_NAME", getName(),
                        "DIMENSION_UNIQUE_NAME",
                        level.olap4jHierarchy.olap4jDimension.getUniqueName(),
                        "HIERARCHY_UNIQUE_NAME",
                        level.olap4jHierarchy.getUniqueName(),
                        "LEVEL_UNIQUE_NAME", level.getUniqueName()
                    });
            return list;
        }
    }
}

// End XmlaOlap4jCube.java
