/*
// 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.*;
import org.olap4j.impl.Olap4jUtil;
import org.olap4j.mdx.*;
import org.olap4j.metadata.*;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import java.io.*;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.*;
import java.sql.Date;
import java.util.*;

import static org.olap4j.driver.xmla.XmlaOlap4jUtil.*;

/**
 * Implementation of {@link org.olap4j.CellSet}
 * for XML/A providers.
 *
 * <p>This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs;
 * it is instantiated using {@link Factory#newCellSet}.</p>
 *
 * @author jhyde
 * @since May 24, 2007
 */
abstract class XmlaOlap4jCellSet implements CellSet {
    private static final String VALUE_TAG = "Value";
    enum XsdTypes {
        XSD_INT("xsd:int"),
        XSD_INTEGER("xsd:integer"),
        XSD_DOUBLE("xsd:double"),
        XSD_POSITIVEINTEGER("xsd:positiveInteger"),
        XSD_DECIMAL("xsd:decimal"),
        XSD_SHORT("xsd:short"),
        XSD_FLOAT("xsd:float"),
        XSD_LONG("xsd:long"),
        XSD_BOOLEAN("xsd:boolean"),
        XSD_BYTE("xsd:byte"),
        XSD_UNSIGNEDBYTE("xsd:unsignedByte"),
        XSD_UNSIGNEDSHORT("xsd:unsignedShort"),
        XSD_UNSIGNEDLONG("xsd:unsignedLong"),
        XSD_UNSIGNEDINT("xsd:unsignedInt"),
        XSD_STRING("xsd:string");

        private final String name;
        private static final Map<String, XsdTypes> TYPES;
        static {
            Map typesInitial = new HashMap<String, XsdTypes>();
            for (XsdTypes type : values()) {
                typesInitial.put(type.name, type);
            }
            TYPES = Collections.unmodifiableMap(typesInitial);
        }

        XsdTypes(String name) {
            this.name = name;
        }

        public static XsdTypes fromString(String name) {
            XsdTypes type = TYPES.get(name);
            return type == null ? XSD_STRING : type;
        }
    }

    final XmlaOlap4jStatement olap4jStatement;
    protected boolean closed;
    private XmlaOlap4jCellSetMetaData metaData;
    private final Map<Integer, Cell> cellMap =
        new HashMap<Integer, Cell>();
    private final List<XmlaOlap4jCellSetAxis> axisList =
        new ArrayList<XmlaOlap4jCellSetAxis>();
    private final List<CellSetAxis> immutableAxisList =
        Olap4jUtil.cast(Collections.unmodifiableList(axisList));
    private XmlaOlap4jCellSetAxis filterAxis;
    private static final boolean DEBUG = false;

    private static final List<String> standardProperties = Arrays.asList(
        "UName", "Caption", "LName", "LNum", "DisplayInfo");

    /**
     * Creates an XmlaOlap4jCellSet.
     *
     * @param olap4jStatement Statement
     */
    XmlaOlap4jCellSet(
        XmlaOlap4jStatement olap4jStatement)
    {
        assert olap4jStatement != null;
        this.olap4jStatement = olap4jStatement;
        this.closed = false;
    }

    /**
     * Returns the error-handler.
     *
     * @return Error handler
     */
    private XmlaHelper getHelper() {
        return olap4jStatement.olap4jConnection.helper;
    }

    /**
     * Gets response from the XMLA request and populates cell set axes and cells
     * with it.
     *
     * @throws OlapException on error
     */
    void populate() throws OlapException {
        byte[] bytes = olap4jStatement.getBytes();

        Document doc;
        try {
            doc = parse(bytes);
        } catch (IOException e) {
            throw getHelper().createException(
                "error creating CellSet", e);
        } catch (SAXException e) {
            throw getHelper().createException(
                "error creating CellSet", e);
        }
        // <SOAP-ENV:Envelope>
        //   <SOAP-ENV:Header/>
        //   <SOAP-ENV:Body>
        //     <xmla:ExecuteResponse>
        //       <xmla:return>
        //         <root>
        //           (see below)
        //         </root>
        //       <xmla:return>
        //     </xmla:ExecuteResponse>
        //   </SOAP-ENV:Body>
        // </SOAP-ENV:Envelope>
        final Element envelope = doc.getDocumentElement();
        if (DEBUG) {
            System.out.println(XmlaOlap4jUtil.toString(doc, true));
        }
        assert envelope.getLocalName().equals("Envelope");
        assert envelope.getNamespaceURI().equals(SOAP_NS);
        Element body =
            findChild(envelope, SOAP_NS, "Body");
        Element fault =
            findChild(body, SOAP_NS, "Fault");
        if (fault != null) {
            // <SOAP-ENV:Fault>
            //     <faultcode>SOAP-ENV:Client.00HSBC01</faultcode>
            //     <faultstring>XMLA connection datasource not
            //                  found</faultstring>
            //     <faultactor>Mondrian</faultactor>
            //     <detail>
            //         <XA:error xmlns:XA="http://mondrian.sourceforge.net">
            //             <code>00HSBC01</code>
            //             <desc>The Mondrian XML: Mondrian Error:Internal
            //                 error: no catalog named 'LOCALDB'</desc>
            //         </XA:error>
            //     </detail>
            // </SOAP-ENV:Fault>
            //
            // TODO: log doc to logfile
            throw getHelper().createException(
                "XMLA provider gave exception: "
                + XmlaOlap4jUtil.prettyPrint(fault));
        }
        Element executeResponse =
            findChild(body, XMLA_NS, "ExecuteResponse");
        Element returnElement =
            findChild(executeResponse, XMLA_NS, "return");
        // <root> has children
        //   <xsd:schema/>
        //   <OlapInfo>
        //     <CubeInfo>
        //       <Cube>
        //         <CubeName>FOO</CubeName>
        //       </Cube>
        //     </CubeInfo>
        //     <AxesInfo>
        //       <AxisInfo/> ...
        //     </AxesInfo>
        //   </OlapInfo>
        //   <Axes>
        //      <Axis>
        //        <Tuples>
        //      </Axis>
        //      ...
        //   </Axes>
        //   <CellData>
        //      <Cell/>
        //      ...
        //   </CellData>
        final Element root =
            findChild(returnElement, MDDATASET_NS, "root");

        if (olap4jStatement instanceof XmlaOlap4jPreparedStatement) {
            this.metaData =
                ((XmlaOlap4jPreparedStatement) olap4jStatement)
                    .cellSetMetaData;
        } else {
            this.metaData = createMetaData(root);
        }

        // todo: use CellInfo element to determine mapping of cell properties
        // to XML tags
        //
        //  <CellInfo>
        //      <Value name="VALUE"/>
        //      <FmtValue name="FORMATTED_VALUE"/>
        //      <FormatString name="FORMAT_STRING"/>
        //  </CellInfo>
        final Element axesNode = findChild(root, MDDATASET_NS, "Axes");

        // First pass, gather up a list of member unique names to fetch
        // all at once.
        //
        // NOTE: This approach allows the driver to fetch a large number
        // of members in one round trip, which is much more efficient.
        // However, if the axis has a very large number of members, the map
        // may use too much memory. This is an unresolved issue.
        final MetadataReader metadataReader =
            metaData.cube.getMetadataReader();
        final Map<String, XmlaOlap4jMember> memberMap =
            new HashMap<String, XmlaOlap4jMember>();
        List<String> uniqueNames = new ArrayList<String>();
        for (Element axisNode : findChildren(axesNode, MDDATASET_NS, "Axis")) {
            final Element tuplesNode =
                findChild(axisNode, MDDATASET_NS, "Tuples");

            for (Element tupleNode
                : findChildren(tuplesNode, MDDATASET_NS, "Tuple"))
            {
                for (Element memberNode
                    : findChildren(tupleNode, MDDATASET_NS, "Member"))
                {
                    final String uname = stringElement(memberNode, "UName");
                    uniqueNames.add(uname);
                }
            }
        }

        // Fetch all members on all axes. Hopefully it can all be done in one
        // round trip, or they are in cache already.
        metadataReader.lookupMembersByUniqueName(uniqueNames, memberMap);

        // Second pass, populate the axis.
        final Map<Property, Object> propertyValues =
            new HashMap<Property, Object>();
        for (Element axisNode : findChildren(axesNode, MDDATASET_NS, "Axis")) {
            final String axisName = axisNode.getAttribute("name");
            final Axis axis = lookupAxis(axisName);
            final ArrayList<Position> positions = new ArrayList<Position>();
            final XmlaOlap4jCellSetAxis cellSetAxis =
                new XmlaOlap4jCellSetAxis(
                    this, axis, Collections.unmodifiableList(positions));
            if (axis.isFilter()) {
                filterAxis = cellSetAxis;
            } else {
                axisList.add(cellSetAxis);
            }
            final Element tuplesNode =
                findChild(axisNode, MDDATASET_NS, "Tuples");
            for (Element tupleNode
                : findChildren(tuplesNode, MDDATASET_NS, "Tuple"))
            {
                final List<Member> members = new ArrayList<Member>();
                for (Element memberNode
                    : findChildren(tupleNode, MDDATASET_NS, "Member"))
                {
                    String hierarchyName =
                        memberNode.getAttribute("Hierarchy");
                    final String uname = stringElement(memberNode, "UName");
                    XmlaOlap4jMemberBase member = memberMap.get(uname);
                    if (member == null) {
                        final String caption =
                            stringElement(memberNode, "Caption");
                        final int lnum = integerElement(memberNode, "LNum");
                        final Hierarchy hierarchy =
                            lookupHierarchy(metaData.cube, hierarchyName);
                        final Level level = hierarchy.getLevels().get(lnum);
                        member = new XmlaOlap4jSurpriseMember(
                            this, level, hierarchy, lnum, caption, uname);
                    }
                    propertyValues.clear();
                    for (Element childNode : childElements(memberNode)) {
                        XmlaOlap4jCellSetMemberProperty property =
                            ((XmlaOlap4jCellSetAxisMetaData)
                                cellSetAxis.getAxisMetaData()).lookupProperty(
                                    hierarchyName,
                                    childNode.getLocalName());
                        if (property != null) {
                            String value = childNode.getTextContent();
                            propertyValues.put(property, value);
                        }
                    }
                    if (!propertyValues.isEmpty()) {
                        member =
                            new XmlaOlap4jPositionMember(
                                member, propertyValues);
                    }
                    members.add(member);
                }
                positions.add(
                    new XmlaOlap4jPosition(
                        members, positions.size()));
            }
        }

        // If XMLA did not return a filter axis, it means that there was no
        // WHERE. This is equivalent to a slicer axis with one tuple that has
        // zero positions. (Versions of Mondrian before 3.4 do, in fact, return
        // a slicer axis with one empty position. This CellSet should behave the
        // same.)
        if (filterAxis == null) {
            filterAxis =
                new XmlaOlap4jCellSetAxis(
                    this,
                    Axis.FILTER,
                    Collections.<Position>singletonList(
                        new XmlaOlap4jPosition(
                            Collections.<Member>emptyList(),
                            0)));
        }

        final Element cellDataNode = findChild(root, MDDATASET_NS, "CellData");
        for (Element cell : findChildren(cellDataNode, MDDATASET_NS, "Cell")) {
            propertyValues.clear();
            final int cellOrdinal =
                Integer.valueOf(cell.getAttribute("CellOrdinal"));
            final Object value = getTypedValue(cell);
            final String formattedValue = stringElement(cell, "FmtValue");
            final String formatString = stringElement(cell, "FormatString");
            Olap4jUtil.discard(formatString);
            for (Element element : childElements(cell)) {
                String tag = element.getLocalName();
                final Property property =
                    metaData.propertiesByTag.get(tag);
                if (property != null) {
                    propertyValues.put(property, element.getTextContent());
                }
            }
            cellMap.put(
                cellOrdinal,
                new XmlaOlap4jCell(
                    this,
                    cellOrdinal,
                    value,
                    formattedValue,
                    propertyValues));
        }
    }

    /**
     * Returns the value of a cell, cast to the appropriate Java object type
     * corresponding to the XML schema (XSD) type of the value.
     *
     * <p>The value type must conform to XSD definitions of the XML element. See
     * <a href="http://books.xmlschemata.org/relaxng/relax-CHP-19.html">RELAX
     * NG, Chapter 19</a> for a full list of possible data types.
     *
     * <p>This method does not currently support all types; most numeric types
     * are supported, but no dates are yet supported. Those not supported
     * fall back to Strings.
     *
     * @param cell The cell of which we want the casted object.
     * @return The object with a correct value.
     * @throws OlapException if any error is encountered while casting the cell
     * value
     */
     private Object getTypedValue(Element cell) throws OlapException {
        Element elm = findChild(cell, MDDATASET_NS, "Value");
        if (elm == null) {
            // Cell is null.
            return null;
        }

        // The object type is contained in xsi:type attribute.
        String type = elm.getAttribute("xsi:type");
        XsdTypes xsdType =  XsdTypes.fromString(elm.getAttribute("xsi:type"));

        try {
            switch (xsdType) {
            case XSD_BOOLEAN:
                return XmlaOlap4jUtil.booleanElement(cell, VALUE_TAG);
            case XSD_INT:
                return XmlaOlap4jUtil.intElement(cell, VALUE_TAG);
            case XSD_INTEGER:
                return XmlaOlap4jUtil.bigIntegerElement(cell, VALUE_TAG);
            case XSD_DOUBLE:
                return XmlaOlap4jUtil.doubleElement(cell, VALUE_TAG);
            case XSD_POSITIVEINTEGER:
                return XmlaOlap4jUtil.bigIntegerElement(cell, VALUE_TAG);
            case XSD_DECIMAL:
                return XmlaOlap4jUtil.bigDecimalElement(cell, VALUE_TAG);
            case XSD_SHORT:
                return XmlaOlap4jUtil.shortElement(cell, VALUE_TAG);
            case XSD_FLOAT:
                return XmlaOlap4jUtil.floatElement(cell, VALUE_TAG);
            case XSD_LONG:
                return XmlaOlap4jUtil.longElement(cell, VALUE_TAG);
            case XSD_BYTE:
                return XmlaOlap4jUtil.byteElement(cell, VALUE_TAG);
            case XSD_UNSIGNEDBYTE:
                return XmlaOlap4jUtil.shortElement(cell, VALUE_TAG);
            case XSD_UNSIGNEDSHORT:
                return XmlaOlap4jUtil.intElement(cell, VALUE_TAG);
            case XSD_UNSIGNEDLONG:
                return XmlaOlap4jUtil.bigDecimalElement(cell, VALUE_TAG);
            case XSD_UNSIGNEDINT:
                return XmlaOlap4jUtil.longElement(cell, VALUE_TAG);
            default:
                return XmlaOlap4jUtil.stringElement(cell, VALUE_TAG);
            }
        } catch (Exception e) {
            throw getHelper().createException(
                "Error while casting a cell value to the correct java type for"
                + " its XSD type " + type,
                e);
        }
    }

    /**
     * Creates metadata for a cell set, given the DOM of the XMLA result.
     *
     * @param root Root node of XMLA result
     * @return Metadata describing this cell set
     * @throws OlapException on error
     */
    private XmlaOlap4jCellSetMetaData createMetaData(Element root)
        throws OlapException
    {
        final Element olapInfo =
            findChild(root, MDDATASET_NS, "OlapInfo");
        final Element cubeInfo =
            findChild(olapInfo, MDDATASET_NS, "CubeInfo");
        final Element cubeNode =
            findChild(cubeInfo, MDDATASET_NS, "Cube");
        final Element cubeNameNode =
            findChild(cubeNode, MDDATASET_NS, "CubeName");
        final String cubeName = gatherText(cubeNameNode);

        XmlaOlap4jCube cube =
            lookupCube(
                olap4jStatement.olap4jConnection.olap4jDatabaseMetaData,
                cubeName);
        if (cube == null) {
            throw getHelper().createException(
                "Internal error: cube '" + cubeName + "' not found");
        }
        final Element axesInfo =
            findChild(olapInfo, MDDATASET_NS, "AxesInfo");
        final List<Element> axisInfos =
            findChildren(axesInfo, MDDATASET_NS, "AxisInfo");
        final List<CellSetAxisMetaData> axisMetaDataList =
            new ArrayList<CellSetAxisMetaData>();
        XmlaOlap4jCellSetAxisMetaData filterAxisMetaData = null;
        for (Element axisInfo : axisInfos) {
            final String axisName = axisInfo.getAttribute("name");
            Axis axis = lookupAxis(axisName);
            final List<Element> hierarchyInfos =
                findChildren(axisInfo, MDDATASET_NS, "HierarchyInfo");
            final List<Hierarchy> hierarchyList =
                new ArrayList<Hierarchy>();
            // <OlapInfo>
            //     <AxesInfo>
            //         <AxisInfo name="Axis0">
            //             <HierarchyInfo name="Customers">
            //                 <UName name="[Customers].[MEMBER_UNIQUE_NAME]"/>
            //                 <Caption name="[Customers].[MEMBER_CAPTION]"/>
            //                 <LName name="[Customers].[LEVEL_UNIQUE_NAME]"/>
            //                 <LNum name="[Customers].[LEVEL_NUMBER]"/>
            //                 <DisplayInfo name="[Customers].[DISPLAY_INFO]"/>
            //             </HierarchyInfo>
            //         </AxisInfo>
            //         ...
            //     </AxesInfo>
            //     <CellInfo>
            //         <Value name="VALUE"/>
            //         <FmtValue name="FORMATTED_VALUE"/>
            //         <FormatString name="FORMAT_STRING"/>
            //     </CellInfo>
            // </OlapInfo>
            final List<XmlaOlap4jCellSetMemberProperty> propertyList =
                new ArrayList<XmlaOlap4jCellSetMemberProperty>();
            for (Element hierarchyInfo : hierarchyInfos) {
                final String hierarchyName = hierarchyInfo.getAttribute("name");
                Hierarchy hierarchy = lookupHierarchy(cube, hierarchyName);
                hierarchyList.add(hierarchy);
                for (Element childNode : childElements(hierarchyInfo)) {
                    String tag = childNode.getLocalName();
                    if (standardProperties.contains(tag)) {
                        continue;
                    }
                    final String propertyUniqueName =
                        childNode.getAttribute("name");
                    final XmlaOlap4jCellSetMemberProperty property =
                        new XmlaOlap4jCellSetMemberProperty(
                            propertyUniqueName,
                            hierarchy,
                            tag);
                    propertyList.add(property);
                }
            }
            final XmlaOlap4jCellSetAxisMetaData axisMetaData =
                new XmlaOlap4jCellSetAxisMetaData(
                    olap4jStatement.olap4jConnection,
                    axis,
                    hierarchyList,
                    propertyList);
            if (axis.isFilter()) {
                filterAxisMetaData = axisMetaData;
            } else {
                axisMetaDataList.add(axisMetaData);
            }
        }
        if (filterAxisMetaData == null) {
            filterAxisMetaData =
                new XmlaOlap4jCellSetAxisMetaData(
                    olap4jStatement.olap4jConnection,
                    Axis.FILTER,
                    Collections.<Hierarchy>emptyList(),
                    Collections.<XmlaOlap4jCellSetMemberProperty>emptyList());
        }
        final Element cellInfo =
            findChild(olapInfo, MDDATASET_NS, "CellInfo");
        List<XmlaOlap4jCellProperty> cellProperties =
            new ArrayList<XmlaOlap4jCellProperty>();
        for (Element element : childElements(cellInfo)) {
            cellProperties.add(
                new XmlaOlap4jCellProperty(
                    element.getLocalName(),
                    element.getAttribute("name")));
        }
        return
            new XmlaOlap4jCellSetMetaData(
                olap4jStatement,
                cube,
                filterAxisMetaData,
                axisMetaDataList,
                cellProperties);
    }

    /**
     * Looks up a cube with a given name within the current database,
     * catalog and schema bound to the source connection.
     *
     * <p>If there are several with the same name, returns the first.
     *
     * @param databaseMetaData Database metadata
     * @param cubeName Cube name
     * @return Cube, or null if not found
     * @throws OlapException on error
     */
    private XmlaOlap4jCube lookupCube(
        XmlaOlap4jDatabaseMetaData databaseMetaData,
        String cubeName) throws OlapException
    {
        for (Cube cube
            : databaseMetaData.olap4jConnection.getOlapSchema().getCubes())
        {
            if (cubeName.equals(cube.getName())) {
                return (XmlaOlap4jCube) cube;
            }
            if (cubeName.equals("[" + cube.getName() + "]")) {
                return (XmlaOlap4jCube) cube;
            }
        }
        return null;
    }

    /**
     * Looks up a hierarchy in a cube with a given name or, failing that, a
     * given unique name. Throws if not found.
     *
     * @param cube Cube
     * @param hierarchyName Name (or unique name) of hierarchy.
     * @return Hierarchy
     * @throws OlapException on error
     */
    private Hierarchy lookupHierarchy(XmlaOlap4jCube cube, String hierarchyName)
        throws OlapException
    {
        Hierarchy hierarchy = cube.getHierarchies().get(hierarchyName);
        if (hierarchy == null) {
            for (Hierarchy hierarchy1 : cube.getHierarchies()) {
                if (hierarchy1.getUniqueName().equals(hierarchyName)) {
                    hierarchy = hierarchy1;
                    break;
                }
            }
            if (hierarchy == null) {
                throw getHelper().createException(
                    "Internal error: hierarchy '" + hierarchyName
                    + "' not found in cube '" + cube.getName() + "'");
            }
        }
        return hierarchy;
    }

    /**
     * Looks up an Axis with a given name.
     *
     * @param axisName Name of axis
     * @return Axis
     */
    private Axis lookupAxis(String axisName) {
        if (axisName.startsWith("Axis")) {
            final Integer ordinal =
                Integer.valueOf(axisName.substring("Axis".length()));
            return Axis.Factory.forOrdinal(ordinal);
        } else {
            return Axis.FILTER;
        }
    }

    public CellSetMetaData getMetaData() {
        return metaData;
    }

    public Cell getCell(List<Integer> coordinates) {
        return getCellInternal(coordinatesToOrdinal(coordinates));
    }

    public Cell getCell(int ordinal) {
        return getCellInternal(ordinal);
    }

    public Cell getCell(Position... positions) {
        if (positions.length != getAxes().size()) {
            throw new IllegalArgumentException(
                "cell coordinates should have dimension " + getAxes().size());
        }
        List<Integer> coords = new ArrayList<Integer>(positions.length);
        for (Position position : positions) {
            coords.add(position.getOrdinal());
        }
        return getCell(coords);
    }

    /**
     * Returns a cell given its ordinal.
     *
     * @param pos Ordinal
     * @return Cell
     * @throws IndexOutOfBoundsException if ordinal is not in range
     */
    private Cell getCellInternal(int pos) {
        final Cell cell = cellMap.get(pos);
        if (cell == null) {
            if (pos < 0 || pos >= maxOrdinal()) {
                throw new IndexOutOfBoundsException();
            } else {
                // Cell is within bounds, but is not held in the cache because
                // it has no value. Manufacture a cell with an empty value.
                return new XmlaOlap4jCell(
                    this, pos, null, "",
                    Collections.<Property, Object>emptyMap());
            }
        }
        return cell;
    }

    /**
     * Returns a string describing the maximum coordinates of this cell set;
     * for example "2, 3" for a cell set with 2 columns and 3 rows.
     *
     * @return description of cell set bounds
     */
    private String getBoundsAsString() {
        StringBuilder buf = new StringBuilder();
        int k = 0;
        for (CellSetAxis axis : getAxes()) {
            if (k++ > 0) {
                buf.append(", ");
            }
            buf.append(axis.getPositionCount());
        }
        return buf.toString();
    }

    public List<CellSetAxis> getAxes() {
        return immutableAxisList;
    }

    public CellSetAxis getFilterAxis() {
        return filterAxis;
    }

    /**
     * Returns the ordinal of the last cell in this cell set. This is the
     * product of the cardinalities of all axes.
     *
     * @return ordinal of last cell in cell set
     */
    private int maxOrdinal() {
        int modulo = 1;
        for (CellSetAxis axis : axisList) {
            modulo *= axis.getPositionCount();
        }
        return modulo;
    }

    public List<Integer> ordinalToCoordinates(int ordinal) {
        List<CellSetAxis> axes = getAxes();
        final List<Integer> list = new ArrayList<Integer>(axes.size());
        int modulo = 1;
        for (CellSetAxis axis : axes) {
            int prevModulo = modulo;
            modulo *= axis.getPositionCount();
            list.add((ordinal % modulo) / prevModulo);
        }
        if (ordinal < 0 || ordinal >= modulo) {
            throw new IndexOutOfBoundsException(
                "Cell ordinal " + ordinal
                + ") lies outside CellSet bounds ("
                + getBoundsAsString() + ")");
        }
        return list;
    }

    public int coordinatesToOrdinal(List<Integer> coordinates) {
        List<CellSetAxis> axes = getAxes();
        if (coordinates.size() != axes.size()) {
            throw new IllegalArgumentException(
                "Coordinates have different dimension " + coordinates.size()
                + " than axes " + axes.size());
        }
        int modulo = 1;
        int ordinal = 0;
        int k = 0;
        for (CellSetAxis axis : axes) {
            final Integer coordinate = coordinates.get(k++);
            if (coordinate < 0 || coordinate >= axis.getPositionCount()) {
                throw new IndexOutOfBoundsException(
                    "Coordinate " + coordinate
                    + " of axis " + (k - 1)
                    + " is out of range ("
                    + getBoundsAsString() + ")");
            }
            ordinal += coordinate * modulo;
            modulo *= axis.getPositionCount();
        }
        return ordinal;
    }

    public boolean next() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void close() throws SQLException {
        this.closed = true;
    }

    public boolean wasNull() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public String getString(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean getBoolean(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public byte getByte(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public short getShort(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public int getInt(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public long getLong(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public float getFloat(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public double getDouble(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public BigDecimal getBigDecimal(
        int columnIndex, int scale) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public byte[] getBytes(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Date getDate(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Time getTime(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Timestamp getTimestamp(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public InputStream getAsciiStream(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public InputStream getUnicodeStream(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public InputStream getBinaryStream(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public String getString(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean getBoolean(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public byte getByte(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public short getShort(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public int getInt(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public long getLong(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public float getFloat(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public double getDouble(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public BigDecimal getBigDecimal(
        String columnLabel, int scale) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public byte[] getBytes(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Date getDate(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Time getTime(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Timestamp getTimestamp(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public InputStream getAsciiStream(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public InputStream getUnicodeStream(String columnLabel)
        throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public InputStream getBinaryStream(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public SQLWarning getWarnings() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void clearWarnings() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public String getCursorName() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Object getObject(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Object getObject(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public int findColumn(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Reader getCharacterStream(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Reader getCharacterStream(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public BigDecimal getBigDecimal(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public BigDecimal getBigDecimal(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean isBeforeFirst() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean isAfterLast() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean isFirst() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean isLast() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void beforeFirst() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void afterLast() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean first() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean last() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public int getRow() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean absolute(int row) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean relative(int rows) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean previous() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void setFetchDirection(int direction) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public int getFetchDirection() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void setFetchSize(int rows) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public int getFetchSize() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public int getType() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public int getConcurrency() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean rowUpdated() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean rowInserted() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean rowDeleted() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateNull(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateBoolean(int columnIndex, boolean x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateByte(int columnIndex, byte x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateShort(int columnIndex, short x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateInt(int columnIndex, int x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateLong(int columnIndex, long x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateFloat(int columnIndex, float x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateDouble(int columnIndex, double x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateBigDecimal(
        int columnIndex, BigDecimal x) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateString(int columnIndex, String x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateBytes(int columnIndex, byte x[]) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateDate(int columnIndex, Date x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateTime(int columnIndex, Time x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateTimestamp(
        int columnIndex, Timestamp x) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateAsciiStream(
        int columnIndex, InputStream x, int length) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateBinaryStream(
        int columnIndex, InputStream x, int length) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateCharacterStream(
        int columnIndex, Reader x, int length) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateObject(
        int columnIndex, Object x, int scaleOrLength) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateObject(int columnIndex, Object x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateNull(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateBoolean(
        String columnLabel, boolean x) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateByte(String columnLabel, byte x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateShort(String columnLabel, short x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateInt(String columnLabel, int x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateLong(String columnLabel, long x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateFloat(String columnLabel, float x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateDouble(String columnLabel, double x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateBigDecimal(
        String columnLabel, BigDecimal x) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateString(String columnLabel, String x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateBytes(String columnLabel, byte x[]) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateDate(String columnLabel, Date x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateTime(String columnLabel, Time x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateTimestamp(
        String columnLabel, Timestamp x) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateAsciiStream(
        String columnLabel, InputStream x, int length) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateBinaryStream(
        String columnLabel, InputStream x, int length) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateCharacterStream(
        String columnLabel, Reader reader, int length) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateObject(
        String columnLabel, Object x, int scaleOrLength) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public void updateObject(String columnLabel, Object x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void insertRow() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateRow() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void deleteRow() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void refreshRow() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void cancelRowUpdates() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void moveToInsertRow() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void moveToCurrentRow() throws SQLException {
        throw new UnsupportedOperationException();
    }

    public OlapStatement getStatement() {
        return olap4jStatement;
    }

    public Object getObject(
        int columnIndex, Map<String, Class<?>> map) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public Ref getRef(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Blob getBlob(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Clob getClob(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Array getArray(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Object getObject(
        String columnLabel, Map<String, Class<?>> map) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public Ref getRef(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Blob getBlob(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Clob getClob(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Array getArray(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Date getDate(int columnIndex, Calendar cal) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Date getDate(String columnLabel, Calendar cal) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Time getTime(int columnIndex, Calendar cal) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Time getTime(String columnLabel, Calendar cal) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public Timestamp getTimestamp(
        int columnIndex, Calendar cal) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public Timestamp getTimestamp(
        String columnLabel, Calendar cal) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    public URL getURL(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public URL getURL(String columnLabel) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateRef(int columnIndex, Ref x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateRef(String columnLabel, Ref x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateBlob(int columnIndex, Blob x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateBlob(String columnLabel, Blob x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateClob(int columnIndex, Clob x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateClob(String columnLabel, Clob x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateArray(int columnIndex, Array x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public void updateArray(String columnLabel, Array x) throws SQLException {
        throw new UnsupportedOperationException();
    }

    // implement Wrapper

    public <T> T unwrap(Class<T> iface) throws SQLException {
        throw new UnsupportedOperationException();
    }

    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        throw new UnsupportedOperationException();
    }

    /**
     * Implementation of {@link Member} for a member which is not present
     * in the cube (probably because the member is a calculated member
     * defined in the query).
     */
    private static class XmlaOlap4jSurpriseMember
        implements XmlaOlap4jMemberBase
    {
        private final XmlaOlap4jCellSet cellSet;
        private final Level level;
        private final Hierarchy hierarchy;
        private final int lnum;
        private final String caption;
        private final String uname;
        private Member parentMember;
        private final String parentMemberUniqueName;

        /**
         * Creates an XmlaOlap4jSurpriseMember.
         *
         * @param cellSet Cell set
         * @param level Level
         * @param hierarchy Hierarchy
         * @param lnum Level number
         * @param caption Caption
         * @param uname Member unique name
         */
        XmlaOlap4jSurpriseMember(
            XmlaOlap4jCellSet cellSet,
            Level level,
            Hierarchy hierarchy,
            int lnum,
            String caption,
            String uname)
        {
            this.cellSet = cellSet;
            this.level = level;
            this.hierarchy = hierarchy;
            this.lnum = lnum;
            this.caption = caption;
            this.uname = uname;
            parentMemberUniqueName = getParentUniqueName();
        }

        private String getParentUniqueName() {
            List<IdentifierSegment> segments = IdentifierNode
                .parseIdentifier(getUniqueName()).getSegmentList();
            IdentifierNode parentIdentifier = new IdentifierNode(
                segments.subList(0, segments.size() - 1));
            return parentIdentifier.toString();
        }

        public final XmlaOlap4jCube getCube() {
            return cellSet.metaData.cube;
        }

        public final XmlaOlap4jConnection getConnection() {
            return getCatalog().olap4jDatabaseMetaData.olap4jConnection;
        }

        public final XmlaOlap4jCatalog getCatalog() {
            return getCube().olap4jSchema.olap4jCatalog;
        }

        public Map<Property, Object> getPropertyValueMap() {
            return Collections.emptyMap();
        }

        public NamedList<? extends Member> getChildMembers()
        {
            return Olap4jUtil.emptyNamedList();
        }

        public int getChildMemberCount() {
            return 0;
        }


        /**
         * In the case of a query-calculated member over XMLA, the only
         * information we have to determine parent is this member's own
         * unique name, its level number, and whether .hasAll() is true.
         *
         * This method attempts to figure out the parent member based on
         * what we hope is the unique name of the parent--i.e. the member's
         * own unique name with the last segment stripped off.
         *
         * For calculated members defined directly on the hierarchy,
         * stripping off the last segment does not produce a valid member
         * unique name.  In that case, though, the level depth is 0, which
         * we can take as an indication that the parent is null.
         */
        public Member getParentMember() {
            if (getLevel().getDepth() == 0 || parentMemberUniqueName == null) {
                return null;
            }
            if (parentMember == null) {
                try {
                    parentMember =
                        getCube().getMetadataReader()
                            .lookupMemberByUniqueName(parentMemberUniqueName);
                    if (parentMember == null
                            && getLevel().getDepth() == 1
                            && getHierarchy().hasAll())
                    {
                        // couldn't find the parent by the constructed
                        // unique name, but we know it's (All) in this case.
                        parentMember = getAllMember();
                    }
                } catch (OlapException e) {
                    throw new RuntimeException(
                        "Failed to retrieve parent of " + getName(), e);
                }
            }
            return parentMember;
        }

        private Member getAllMember() throws OlapException {
            if (!getHierarchy().hasAll()) {
                return null;
            }
            List<Member> roots = getHierarchy().getRootMembers();
            assert roots.size() == 1;

            return roots.get(0);
        }

        public Level getLevel() {
            return level;
        }

        public Hierarchy getHierarchy() {
            return hierarchy;
        }

        public Dimension getDimension() {
            return hierarchy.getDimension();
        }

        public Type getMemberType() {
            return Type.UNKNOWN;
        }

        public boolean isAll() {
            return false; // FIXME
        }

        public boolean isChildOrEqualTo(Member member) {
            return false; // FIXME
        }

        public boolean isCalculated() {
            return false; // FIXME
        }

        public int getSolveOrder() {
            return 0; // FIXME
        }

        public ParseTreeNode getExpression() {
            return null;
        }

        public List<Member> getAncestorMembers() {
            return Collections.emptyList(); // FIXME
        }

        public boolean isCalculatedInQuery() {
            return true; // probably
        }

        public Object getPropertyValue(Property property) {
            return null;
        }

        public String getPropertyFormattedValue(Property property) {
            return null;
        }

        public void setProperty(Property property, Object value)
        {
            throw new UnsupportedOperationException();
        }

        public NamedList<Property> getProperties() {
            return Olap4jUtil.emptyNamedList();
        }

        public int getOrdinal() {
            return -1; // FIXME
        }

        public boolean isHidden() {
            return false;
        }

        public int getDepth() {
            return lnum;
        }

        public Member getDataMember() {
            return null;
        }

        public String getName() {
            return caption;
        }

        public String getUniqueName() {
            return uname;
        }

        public String getCaption() {
            return caption;
        }

        public String getDescription() {
            return null;
        }

        public boolean isVisible() {
            return true;
        }
    }
}

// End XmlaOlap4jCellSet.java

