/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.apache.ignite.internal.processors.query.h2.sql;

import java.util.ArrayList;
import java.util.List;
import org.h2.util.StatementBuilder;
import org.h2.util.StringUtils;

/**
 * Plain SELECT query.
 */
public class GridSqlSelect extends GridSqlQuery {
    /** */
    public static final int FROM_CHILD = 2;

    /** */
    public static final int WHERE_CHILD = 3;

    /** */
    private static final int COLS_CHILD = 4;

    /** */
    private List<GridSqlAst> cols = new ArrayList<>();

    /** */
    private int visibleCols;

    /** */
    private boolean distinct;

    /** */
    private int[] grpCols;

    /** */
    private GridSqlAst from;

    /** */
    private GridSqlAst where;

    /** */
    private int havingCol = -1;

    /**
     * @param colIdx Column index as for {@link #column(int)}.
     * @return Child index for {@link #child(int)}.
     */
    public static int childIndexForColumn(int colIdx) {
        return colIdx + COLS_CHILD;
    }

    /** {@inheritDoc} */
    @Override public int size() {
        return 4 + cols.size(); // + FROM + WHERE + OFFSET + LIMIT
    }

    /** {@inheritDoc} */
    @SuppressWarnings("unchecked")
    @Override public <E extends GridSqlAst> E child(int childIdx) {
        if (childIdx < FROM_CHILD)
            return super.child(childIdx);

        switch (childIdx) {
            case FROM_CHILD:
                return maskNull(from, GridSqlPlaceholder.EMPTY);

            case WHERE_CHILD:
                return maskNull(where, GridSqlConst.TRUE);

            default:
                return (E)cols.get(childIdx - COLS_CHILD);
        }
    }

    /** {@inheritDoc} */
    @Override public <E extends GridSqlAst> void child(int childIdx, E child) {
        if (childIdx < FROM_CHILD) {
            super.child(childIdx, child);

            return;
        }

        switch (childIdx) {
            case FROM_CHILD:
                from = child;

                break;

            case WHERE_CHILD:
                where = child;

                break;

            default:
                cols.set(childIdx - COLS_CHILD, child);
        }
    }

    /** {@inheritDoc} */
    @Override public int visibleColumns() {
        return visibleCols;
    }

    /**
     * @return Number of columns is select including invisible ones.
     */
    public int allColumns() {
        return cols.size();
    }

    /** {@inheritDoc} */
    @Override protected GridSqlAst column(int col) {
        return cols.get(col);
    }

    /** {@inheritDoc} */
    @Override public String getSQL() {
        StatementBuilder buff = new StatementBuilder(explain() ? "EXPLAIN SELECT" : "SELECT");

        if (distinct)
            buff.append(" DISTINCT");

        for (GridSqlAst expression : columns(true)) {
            buff.appendExceptFirst(",");
            buff.append('\n');
            buff.append(expression.getSQL());
        }

        if (from != null)
            buff.append("\nFROM ").append(from.getSQL());

        if (where != null)
            buff.append("\nWHERE ").append(StringUtils.unEnclose(where.getSQL()));

        if (grpCols != null) {
            buff.append("\nGROUP BY ");

            buff.resetCount();

            for (int grpCol : grpCols) {
                buff.appendExceptFirst(", ");

                addAlias(buff, cols.get(grpCol));
            }
        }

        if (havingCol >= 0) {
            buff.append("\nHAVING ");

            addAlias(buff, cols.get(havingCol));
        }

        getSortLimitSQL(buff);

        return buff.toString();
    }

    /**
     * @return {@code True} if this simple SQL query like 'SELECT A, B, C from SOME_TABLE' without any conditions
     *      and expressions.
     */
    @Override public boolean simpleQuery() {
        boolean simple = !distinct &&
            from instanceof GridSqlTable &&
            where == null &&
            grpCols == null &&
            havingCol < 0 &&
            sort.isEmpty() &&
            limit() == null &&
            offset() == null;

        if (simple) {
            for (GridSqlAst expression : columns(true)) {
                if (expression instanceof GridSqlAlias)
                    expression = expression.child();

                if (!(expression instanceof GridSqlColumn))
                    return false;
            }
        }

        return simple;
    }

    /**
     * @param buff Statement builder.
     * @param exp Alias expression.
     */
    private static void addAlias(StatementBuilder buff, GridSqlAst exp) {
        exp = GridSqlAlias.unwrap(exp);

        buff.append(StringUtils.unEnclose(exp.getSQL()));
    }

    /**
     * @param visibleOnly If only visible expressions needed.
     * @return Select clause expressions.
     */
    public List<GridSqlAst> columns(boolean visibleOnly) {
        assert visibleCols <= cols.size();

        return visibleOnly && visibleCols != cols.size() ?
            cols.subList(0, visibleCols) : cols;
    }

    /**
     * Clears select expressions list.
     * @return {@code this}.
     */
    public GridSqlSelect clearColumns() {
        visibleCols = 0;
        cols = new ArrayList<>();

        return this;
    }

    /**
     * @param expression Expression.
     * @param visible Expression is visible in select phrase.
     * @return {@code this}.
     */
    public GridSqlSelect addColumn(GridSqlAst expression, boolean visible) {
        if (expression == null)
            throw new NullPointerException();

        if (visible) {
            if (visibleCols != cols.size())
                throw new IllegalStateException("Already started adding invisible columns.");

            visibleCols++;
        }

        cols.add(expression);

        return this;
    }

    /**
     * @param colIdx Column index.
     * @param expression Expression.
     * @return {@code this}.
     */
    public GridSqlSelect setColumn(int colIdx, GridSqlAst expression) {
        if (expression == null)
            throw new NullPointerException();

        cols.set(colIdx, expression);

        return this;
    }

    /**
     * @return Group columns.
     */
    public int[] groupColumns() {
        return grpCols;
    }

    /**
     * @param grpCols Group columns.
     * @return {@code this}.
     */
    public GridSqlSelect groupColumns(int[] grpCols) {
        this.grpCols = grpCols;

        return this;
    }

    /**
     * @return Tables.
     */
    public GridSqlAst from() {
        return from;
    }

    /**
     * @param from From element.
     * @return {@code this}.
     */
    public GridSqlSelect from(GridSqlAst from) {
        this.from = from;

        return this;
    }

    /**
     * @return Distinct.
     */
    public boolean distinct() {
        return distinct;
    }

    /**
     * @param distinct New distinct.
     */
    public void distinct(boolean distinct) {
        this.distinct = distinct;
    }

    /**
     * @return Where.
     */
    public GridSqlAst where() {
        return where;
    }

    /**
     * @param where New where.
     * @return {@code this}.
     */
    public GridSqlSelect where(GridSqlAst where) {
        this.where = where;

        return this;
    }

    /**
     * @param cond Adds new WHERE condition using AND operator.
     * @return {@code this}.
     */
    public GridSqlSelect whereAnd(GridSqlAst cond) {
        if (cond == null)
            throw new NullPointerException();

        GridSqlAst old = where();

        where(old == null ? cond : new GridSqlOperation(GridSqlOperationType.AND, old, cond));

        return this;
    }

    /**
     * @return Having.
     */
    public GridSqlAst having() {
        return havingCol >= 0 ? column(havingCol) : null;
    }

    /**
     * @param col Index of HAVING column.
     * @return {@code this}.
     */
    public GridSqlSelect havingColumn(int col) {
        assert col >= -1 : col;

        havingCol = col;

        return this;
    }

    /**
     * @return Index of HAVING column.
     */
    public int havingColumn() {
        return havingCol;
    }
}