/*
// Licensed to Julian Hyde under one or more contributor license
// agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership.
//
// Julian Hyde licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
*/
package org.olap4j.query;

import org.olap4j.*;
import org.olap4j.mdx.ParseTreeNode;
import org.olap4j.mdx.SelectNode;
import org.olap4j.metadata.*;

import java.sql.SQLException;
import java.util.*;
import java.util.Map.Entry;

/**
 * Base query model object.
 *
 * @author jhyde, jdixon, Luc Boudreau
 * @since May 29, 2007
 */
public class Query extends QueryNodeImpl {

    protected final String name;
    protected Map<Axis, QueryAxis> axes = new HashMap<Axis, QueryAxis>();
    private List<ParseTreeNode> withList;
    protected QueryAxis across;
    protected QueryAxis down;
    protected QueryAxis filter;
    protected QueryAxis unused;
    protected final Cube cube;
    protected Map<String, QueryDimension> dimensionMap =
        new HashMap<String, QueryDimension>();
    /**
     * Whether or not to select the default hierarchy and default
     * member on a dimension if no explicit selections were performed.
     */
    protected boolean selectDefaultMembers = true;
    private final OlapConnection connection;
    private final SelectionFactory selectionFactory = new SelectionFactory();

    /**
     * Constructs a Query object.
     * @param name Any arbitrary name to give to this query.
     * @param cube A Cube object against which to build a query.
     * @throws SQLException If an error occurs while accessing the
     * cube's underlying connection.
     */
    public Query(String name, Cube cube) throws SQLException {
        super();
        this.name = name;
        this.cube = cube;
        final Catalog catalog = cube.getSchema().getCatalog();
        this.connection =
            catalog.getMetaData().getConnection().unwrap(OlapConnection.class);
        this.connection.setCatalog(catalog.getName());
        this.unused = new QueryAxis(this, null);
        for (Dimension dimension : cube.getDimensions()) {
            QueryDimension queryDimension = new QueryDimension(
                this, dimension);
            unused.getDimensions().add(queryDimension);
            dimensionMap.put(queryDimension.getName(), queryDimension);
        }
        across = new QueryAxis(this, Axis.COLUMNS);
        down = new QueryAxis(this, Axis.ROWS);
        filter = new QueryAxis(this, Axis.FILTER);
        axes.put(null, unused);
        axes.put(Axis.COLUMNS, across);
        axes.put(Axis.ROWS, down);
        axes.put(Axis.FILTER, filter);
        withList = new ArrayList<ParseTreeNode>();
    }

    /**
     * Returns the MDX parse tree behind this Query. The returned object is
     * generated for each call to this function. Altering the returned
     * SelectNode object won't affect the query itself.
     * @return A SelectNode object representing the current query structure.
     */
    public SelectNode getSelect() {
        return Olap4jNodeConverter.toOlap4j(this);
    }

    /**
     * Returns the underlying cube object that is used to query against.
     * @return The Olap4j's Cube object.
     */
    public Cube getCube() {
        return cube;
    }

    /**
     * Returns the Olap4j's Dimension object according to the name
     * given as a parameter. If no dimension of the given name is found,
     * a null value will be returned.
     * @param name The name of the dimension you want the object for.
     * @return The dimension object, null if no dimension of that
     * name can be found.
     */
    public QueryDimension getDimension(String name) {
        return dimensionMap.get(name);
    }

    /**
     * Swaps rows and columns axes. Only applicable if there are two axes.
     */
    public void swapAxes() {
        // Only applicable if there are two axes - plus filter and unused.
        if (axes.size() != 4) {
            throw new IllegalArgumentException();
        }
        List<QueryDimension> tmpAcross = new ArrayList<QueryDimension>();
        tmpAcross.addAll(across.getDimensions());

        List<QueryDimension> tmpDown = new ArrayList<QueryDimension>();
        tmpDown.addAll(down.getDimensions());

        across.getDimensions().clear();
        Map<Integer, QueryNode> acrossChildList =
            new HashMap<Integer, QueryNode>();
        for (int cpt = 0; cpt < tmpAcross.size();cpt++) {
            acrossChildList.put(Integer.valueOf(cpt), tmpAcross.get(cpt));
        }
        across.notifyRemove(acrossChildList);

        down.getDimensions().clear();
        Map<Integer, QueryNode> downChildList =
            new HashMap<Integer, QueryNode>();
        for (int cpt = 0; cpt < tmpDown.size();cpt++) {
            downChildList.put(Integer.valueOf(cpt), tmpDown.get(cpt));
        }
        down.notifyRemove(downChildList);

        across.getDimensions().addAll(tmpDown);
        across.notifyAdd(downChildList);

        down.getDimensions().addAll(tmpAcross);
        down.notifyAdd(acrossChildList);
    }

    /**
     * Returns the query axis for a given axis type.
     *
     * <p>If you pass axis=null, returns a special axis that is used to hold
     * all unused hierarchies. (We may change this behavior in future.)
     *
     * @param axis Axis type
     * @return Query axis
     */
    public QueryAxis getAxis(Axis axis) {
        return this.axes.get(axis);
    }

    /**
     * Returns a map of the current query's axis.
     * <p>Be aware that modifications to this list might
     * have unpredictable consequences.</p>
     * @return A standard Map object that represents the
     * current query's axis.
     */
    public Map<Axis, QueryAxis> getAxes() {
        return axes;
    }

    /**
     * Returns the fictional axis into which all unused dimensions are stored.
     * All dimensions included in this axis will not be part of the query.
     * @return The QueryAxis representing dimensions that are currently not
     * used inside the query.
     */
    public QueryAxis getUnusedAxis() {
        return unused;
    }

    /**
     * Safely disposes of all underlying objects of this
     * query.
     * @param closeConnection Whether or not to call the
     * {@link OlapConnection#close()} method of the underlying
     * connection.
     */
    public void tearDown(boolean closeConnection) {
        for (Entry<Axis, QueryAxis> entry : this.axes.entrySet()) {
            entry.getValue().tearDown();
        }
        this.axes.clear();
        this.clearListeners();
        if (closeConnection) {
            try {
                this.connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Safely disposes of all underlying objects of this
     * query and closes the underlying {@link OlapConnection}.
     * <p>Equivalent of calling Query.tearDown(true).
     */
    public void tearDown() {
        this.tearDown(true);
    }

    /**
     * Validates the current query structure. If a dimension axis has
     * been placed on an axis but no selections were performed on it,
     * the default hierarchy and default member will be selected. This
     * can be turned off by invoking the
     * {@link Query#setSelectDefaultMembers(boolean)} method.
     * @throws OlapException If the query is not valid, an exception
     * will be thrown and it's message will describe exactly what to fix.
     */
    public void validate() throws OlapException {
        try {
            // First, perform default selections if needed.
            if (this.selectDefaultMembers) {
                // Perform default selection on the dimensions on the rows axis.
                for (QueryDimension dimension : this.getAxis(Axis.ROWS)
                    .getDimensions())
                {
                    if (dimension.getInclusions().size() == 0) {
                        Member defaultMember = dimension.getDimension()
                            .getDefaultHierarchy().getDefaultMember();
                        dimension.include(defaultMember);
                    }
                }
                // Perform default selection on the
                // dimensions on the columns axis.
                for (QueryDimension dimension : this.getAxis(Axis.COLUMNS)
                    .getDimensions())
                {
                    if (dimension.getInclusions().size() == 0) {
                        Member defaultMember = dimension.getDimension()
                            .getDefaultHierarchy().getDefaultMember();
                        dimension.include(defaultMember);
                    }
                }
                // Perform default selection on the dimensions
                // on the filter axis.
                for (QueryDimension dimension : this.getAxis(Axis.FILTER)
                    .getDimensions())
                {
                    if (dimension.getInclusions().size() == 0) {
                        Member defaultMember = dimension.getDimension()
                            .getDefaultHierarchy().getDefaultMember();
                        dimension.include(defaultMember);
                    }
                }
            }

            // We at least need a dimension on the rows and on the columns axis.
            if (this.getAxis(Axis.ROWS).getDimensions().size() == 0) {
                throw new OlapException(
                    "A valid Query requires at least one dimension on the rows axis.");
            }
            if (this.getAxis(Axis.COLUMNS).getDimensions().size() == 0) {
                throw new OlapException(
                    "A valid Query requires at least one dimension on the columns axis.");
            }

            // Try to build a select tree.
            this.getSelect();
        } catch (Exception e) {
            throw new OlapException("Query validation failed.", e);
        }
    }

    /**
     * Executes the query against the current OlapConnection and returns
     * a CellSet object representation of the data.
     *
     * @return A proper CellSet object that represents the query execution
     *     results.
     * @throws OlapException If something goes sour, an OlapException will
     *     be thrown to the caller. It could be caused by many things, like
     *     a stale connection. Look at the root cause for more details.
     */
    public CellSet execute() throws OlapException {
        SelectNode mdx = getSelect();
        final Catalog catalog = cube.getSchema().getCatalog();
        try {
            this.connection.setCatalog(catalog.getName());
        } catch (SQLException e) {
            throw new OlapException("Error while executing query", e);
        }
        OlapStatement olapStatement = connection.createStatement();
        return olapStatement.executeOlapQuery(mdx);
    }

    /**
     * Returns this query's name. There is no guarantee that it is unique
     * and is set at object instanciation.
     * @return This query's name.
     */
    public String getName() {
        return name;
    }

    /**
     * Returns the current locale with which this query is expressed.
     * @return A standard Locale object.
     */
    public Locale getLocale() {
        // REVIEW Do queries really support locales?
        return Locale.getDefault();
    }

    /**
     * Package restricted method to access this query's selection factory.
     * Usually used by query dimensions who wants to perform selections.
     * @return The underlying SelectionFactory implementation.
     */
    SelectionFactory getSelectionFactory() {
        return selectionFactory;
    }

    /**
     * Behavior setter for a query. By default, if a dimension is placed on
     * an axis but no selections are made, the default hierarchy and
     * the default member will be selected when validating the query.
     * This behavior can be turned off by this setter.
     * @param selectDefaultMembers Enables or disables the default
     * member and hierarchy selection upon validation.
     */
    public void setSelectDefaultMembers(boolean selectDefaultMembers) {
        this.selectDefaultMembers = selectDefaultMembers;
    }

    public List<ParseTreeNode> getWithList() {
        return withList;
    }

    public void setWithList(List<ParseTreeNode> withList) {
        this.withList = withList;
    }
}

// End Query.java
