/*
 * 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.phoenix.util;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import java.util.Map.Entry;

import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.compile.ExpressionCompiler;
import org.apache.phoenix.compile.OrderPreservingTracker.Info;
import org.apache.phoenix.compile.GroupByCompiler.GroupBy;
import org.apache.phoenix.compile.OrderByCompiler.OrderBy;
import org.apache.phoenix.expression.AndExpression;
import org.apache.phoenix.expression.ColumnExpression;
import org.apache.phoenix.expression.ComparisonExpression;
import org.apache.phoenix.expression.Determinism;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.expression.IsNullExpression;
import org.apache.phoenix.expression.LiteralExpression;
import org.apache.phoenix.expression.OrderByExpression;
import org.apache.phoenix.expression.RowKeyColumnExpression;
import org.apache.phoenix.expression.visitor.StatelessTraverseAllExpressionVisitor;
import org.apache.phoenix.expression.visitor.StatelessTraverseNoExpressionVisitor;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.schema.ColumnRef;
import org.apache.phoenix.schema.PColumn;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.PTableType;
import org.apache.phoenix.schema.ProjectedColumn;
import org.apache.phoenix.schema.RowKeyValueAccessor;
import org.apache.phoenix.schema.TableRef;
import org.apache.phoenix.schema.types.PBoolean;
import org.apache.phoenix.schema.types.PDataType;

public class ExpressionUtil {
	private ExpressionUtil() {
	}

	public static boolean isConstant(Expression expression) {
		return (expression.isStateless() && isContantForStatement(expression));
	}

   /**
    * this method determines if expression is constant if all children of it are constants.
    * @param expression
    * @return
    */
    public static boolean isContantForStatement(Expression expression) {
        return  (expression.getDeterminism() == Determinism.ALWAYS
                || expression.getDeterminism() == Determinism.PER_STATEMENT);
    }

    public static LiteralExpression getConstantExpression(Expression expression, ImmutableBytesWritable ptr)
            throws SQLException {
        Object value = null;
        PDataType type = expression.getDataType();
        if (expression.evaluate(null, ptr) && ptr.getLength() != 0) {
            value = type.toObject(ptr.get(), ptr.getOffset(), ptr.getLength(), type, expression.getSortOrder(), expression.getMaxLength(), expression.getScale());
        }
        return LiteralExpression.newConstant(value, type, expression.getDeterminism());
    }

    public static boolean isNull(Expression expression, ImmutableBytesWritable ptr) {
        return isConstant(expression) && (!expression.evaluate(null, ptr) || ptr.getLength() == 0);
    }

    public static LiteralExpression getNullExpression(Expression expression) throws SQLException {
        return LiteralExpression.newConstant(null, expression.getDataType(), expression.getDeterminism());
    }
    
    public static boolean evaluatesToTrue(Expression expression) {
        if (isConstant(expression)) {
            ImmutableBytesWritable ptr = new ImmutableBytesWritable();
            expression.evaluate(null, ptr);
            return Boolean.TRUE.equals(PBoolean.INSTANCE.toObject(ptr));
        }
        return false;
    }

    public static boolean isPkPositionChanging(TableRef tableRef, List<Expression> projectedExpressions) throws SQLException {
        for (int i = 0; i < tableRef.getTable().getPKColumns().size(); i++) {
            PColumn column = tableRef.getTable().getPKColumns().get(i);
            Expression source = projectedExpressions.get(i);
            if (source == null || !source
                    .equals(new ColumnRef(tableRef, column.getPosition()).newColumnExpression())) { return true; }
        }
        return false;
    }

    /**
     * check the whereExpression to see if the columnExpression is constant.
     * eg. for "where a =3 and b > 9", a is constant,but b is not.
     * @param columnExpression
     * @param whereExpression
     * @return
     */
    public static boolean isColumnExpressionConstant(ColumnExpression columnExpression, Expression whereExpression) {
        if(whereExpression == null) {
            return false;
        }
        IsColumnConstantExpressionVisitor isColumnConstantExpressionVisitor =
                new IsColumnConstantExpressionVisitor(columnExpression);
        whereExpression.accept(isColumnConstantExpressionVisitor);
        return isColumnConstantExpressionVisitor.isConstant();
    }

    private static class IsColumnConstantExpressionVisitor extends StatelessTraverseNoExpressionVisitor<Void> {
        private final Expression columnExpression ;
        private Expression firstRhsConstantExpression = null;
        private int rhsConstantCount = 0;
        private boolean isNullExpressionVisited = false;

        public IsColumnConstantExpressionVisitor(Expression columnExpression) {
            this.columnExpression = columnExpression;
        }
        /**
         * only consider and,for "where a = 3 or b = 9", neither a or b is constant.
         */
        @Override
        public Iterator<Expression> visitEnter(AndExpression andExpression) {
            if(rhsConstantCount > 1) {
                return null;
            }
            return andExpression.getChildren().iterator();
        }
        /**
         * <pre>
         * We just consider {@link ComparisonExpression} because:
         * 1.for {@link InListExpression} as "a in ('2')", the {@link InListExpression} is rewritten to
         *  {@link ComparisonExpression} in {@link InListExpression#create}
         * 2.for {@link RowValueConstructorExpression} as "(a,b)=(1,2)",{@link RowValueConstructorExpression}
         *   is rewritten to {@link ComparisonExpression} in {@link ComparisonExpression#create}
         * 3.not consider {@link CoerceExpression}, because for "where cast(a as integer)=2", when a is double,
         *   a is not constant.
         * </pre>
         */
        @Override
        public Iterator<Expression> visitEnter(ComparisonExpression comparisonExpression) {
            if(rhsConstantCount > 1) {
                return null;
            }
            if(comparisonExpression.getFilterOp() != CompareOp.EQUAL) {
                return null;
            }
            Expression lhsExpresssion = comparisonExpression.getChildren().get(0);
            if(!this.columnExpression.equals(lhsExpresssion)) {
                return null;
            }
            Expression rhsExpression = comparisonExpression.getChildren().get(1);
            if(rhsExpression == null) {
                return null;
            }
            Boolean isConstant = rhsExpression.accept(new IsCompositeLiteralExpressionVisitor());
            if(isConstant != null && isConstant.booleanValue()) {
                checkConstantValue(rhsExpression);
            }
            return null;
        }

        public boolean isConstant() {
            return this.rhsConstantCount == 1;
        }

        @Override
        public Iterator<Expression> visitEnter(IsNullExpression isNullExpression) {
            if(rhsConstantCount > 1) {
                return null;
            }
            if(isNullExpression.isNegate()) {
                return null;
            }
            Expression lhsExpresssion = isNullExpression.getChildren().get(0);
            if(!this.columnExpression.equals(lhsExpresssion)) {
                return null;
            }
            this.checkConstantValue(null);
            return null;
        }

        private void checkConstantValue(Expression rhsExpression) {
            if(!this.isNullExpressionVisited && this.firstRhsConstantExpression == null) {
                this.firstRhsConstantExpression = rhsExpression;
                rhsConstantCount++;
                if(rhsExpression == null) {
                    this.isNullExpressionVisited = true;
                }
                return;
            }

            if(!isExpressionEquals(this.isNullExpressionVisited ? null : this.firstRhsConstantExpression, rhsExpression)) {
                rhsConstantCount++;
                return;
            }
        }

        private static boolean isExpressionEquals(Expression oldExpression,Expression newExpression) {
            if(oldExpression == null) {
                if(newExpression == null) {
                    return true;
                }
                return ExpressionUtil.isNull(newExpression, new ImmutableBytesWritable());
            }
            if(newExpression == null) {
                return ExpressionUtil.isNull(oldExpression, new ImmutableBytesWritable());
            }
            return oldExpression.equals(newExpression);
        }
    }

    private static class IsCompositeLiteralExpressionVisitor extends StatelessTraverseAllExpressionVisitor<Boolean> {
        @Override
        public Boolean defaultReturn(Expression expression, List<Boolean> childResultValues) {
            if (!ExpressionUtil.isContantForStatement(expression) ||
                    childResultValues.size() < expression.getChildren().size()) {
                return Boolean.FALSE;
            }
            for (Boolean childResultValue : childResultValues) {
                if (!childResultValue) {
                    return Boolean.FALSE;
                }
            }
            return Boolean.TRUE;
        }
        @Override
        public Boolean visit(LiteralExpression literalExpression) {
            return Boolean.TRUE;
        }
    }

    /**
     * <pre>
     * Infer OrderBys from the rowkey columns of {@link PTable},for projected table may be there is no rowkey columns,
     * so we should move forward to inspect {@link ProjectedColumn} by {@link #getOrderByFromProjectedTable}.
     * The second part of the return pair is the rowkey column offset we must skip when we create OrderBys, because for table with salted/multiTenant/viewIndexId,
     * some leading rowkey columns should be skipped.
     * </pre>
     * @param tableRef
     * @param phoenixConnection
     * @param orderByReverse
     * @return
     * @throws SQLException
     */
    public static Pair<OrderBy,Integer> getOrderByFromTable(
            TableRef tableRef,
            PhoenixConnection phoenixConnection,
            boolean orderByReverse) throws SQLException {

        PTable table = tableRef.getTable();
        Pair<OrderBy,Integer> orderByAndRowKeyColumnOffset =
                getOrderByFromTableByRowKeyColumn(table, phoenixConnection, orderByReverse);
        if(orderByAndRowKeyColumnOffset.getFirst() != OrderBy.EMPTY_ORDER_BY) {
            return orderByAndRowKeyColumnOffset;
        }
        if(table.getType() == PTableType.PROJECTED) {
            orderByAndRowKeyColumnOffset =
                    getOrderByFromProjectedTable(tableRef, phoenixConnection, orderByReverse);
            if(orderByAndRowKeyColumnOffset.getFirst() != OrderBy.EMPTY_ORDER_BY) {
                return orderByAndRowKeyColumnOffset;
            }
        }
        return new Pair<OrderBy,Integer>(OrderBy.EMPTY_ORDER_BY, 0);
    }

    /**
     * Infer OrderBys from the rowkey columns of {@link PTable}.
     * The second part of the return pair is the rowkey column offset we must skip when we create OrderBys, because for table with salted/multiTenant/viewIndexId,
     * some leading rowkey columns should be skipped.
     * @param table
     * @param phoenixConnection
     * @param orderByReverse
     * @return
     */
    public static Pair<OrderBy,Integer> getOrderByFromTableByRowKeyColumn(
            PTable table,
            PhoenixConnection phoenixConnection,
            boolean orderByReverse) {
        Pair<List<RowKeyColumnExpression>,Integer> rowKeyColumnExpressionsAndRowKeyColumnOffset =
                ExpressionUtil.getRowKeyColumnExpressionsFromTable(table, phoenixConnection);
        List<RowKeyColumnExpression> rowKeyColumnExpressions = rowKeyColumnExpressionsAndRowKeyColumnOffset.getFirst();
        int rowKeyColumnOffset = rowKeyColumnExpressionsAndRowKeyColumnOffset.getSecond();
        if(rowKeyColumnExpressions.isEmpty()) {
            return new Pair<OrderBy,Integer>(OrderBy.EMPTY_ORDER_BY,0);
        }
        return new Pair<OrderBy,Integer>(
                convertRowKeyColumnExpressionsToOrderBy(rowKeyColumnExpressions, orderByReverse),
                rowKeyColumnOffset);
    }

    /**
     * For projected table may be there is no rowkey columns,
     * so we should move forward to inspect {@link ProjectedColumn} to check if the source column is rowkey column.
     * The second part of the return pair is the rowkey column offset we must skip when we create OrderBys, because for table with salted/multiTenant/viewIndexId,
     * some leading rowkey columns should be skipped.
     * @param projectedTableRef
     * @param phoenixConnection
     * @param orderByReverse
     * @return
     * @throws SQLException
     */
    public static Pair<OrderBy,Integer> getOrderByFromProjectedTable(
            TableRef projectedTableRef,
            PhoenixConnection phoenixConnection,
            boolean orderByReverse) throws SQLException {

        PTable projectedTable = projectedTableRef.getTable();
        assert projectedTable.getType() == PTableType.PROJECTED;
        TableRef sourceTableRef = null;
        TreeMap<Integer,ColumnRef> sourceRowKeyColumnIndexToProjectedColumnRef =
                new TreeMap<Integer, ColumnRef>();

        for(PColumn column : projectedTable.getColumns()) {
            if(!(column instanceof ProjectedColumn)) {
                continue;
            }
            ProjectedColumn projectedColumn = (ProjectedColumn)column;
            ColumnRef sourceColumnRef = projectedColumn.getSourceColumnRef();
            TableRef currentSourceTableRef = sourceColumnRef.getTableRef();
            if(sourceTableRef == null) {
                sourceTableRef = currentSourceTableRef;
            }
            else if(!sourceTableRef.equals(currentSourceTableRef)) {
                return new Pair<OrderBy,Integer>(OrderBy.EMPTY_ORDER_BY, 0);
            }
            int sourceRowKeyColumnIndex = sourceColumnRef.getPKSlotPosition();
            if(sourceRowKeyColumnIndex >= 0) {
                ColumnRef projectedColumnRef =
                        new ColumnRef(projectedTableRef, projectedColumn.getPosition());
                sourceRowKeyColumnIndexToProjectedColumnRef.put(
                        Integer.valueOf(sourceRowKeyColumnIndex), projectedColumnRef);
            }
        }

        if(sourceTableRef == null) {
            return new Pair<OrderBy,Integer>(OrderBy.EMPTY_ORDER_BY, 0);
        }

        final int sourceRowKeyColumnOffset = getRowKeyColumnOffset(sourceTableRef.getTable(), phoenixConnection);
        List<OrderByExpression> orderByExpressions = new LinkedList<OrderByExpression>();
        int matchedSourceRowKeyColumnOffset = sourceRowKeyColumnOffset;
        for(Entry<Integer,ColumnRef> entry : sourceRowKeyColumnIndexToProjectedColumnRef.entrySet()) {
            int currentRowKeyColumnOffset = entry.getKey();
            if(currentRowKeyColumnOffset < matchedSourceRowKeyColumnOffset) {
                continue;
            }
            else if(currentRowKeyColumnOffset == matchedSourceRowKeyColumnOffset) {
                matchedSourceRowKeyColumnOffset++;
            }
            else {
                break;
            }

            ColumnRef projectedColumnRef = entry.getValue();
            Expression projectedValueColumnExpression = projectedColumnRef.newColumnExpression();
            OrderByExpression orderByExpression =
                    OrderByExpression.convertExpressionToOrderByExpression(projectedValueColumnExpression, orderByReverse);
            orderByExpressions.add(orderByExpression);
        }

        if(orderByExpressions.isEmpty()) {
            return new Pair<OrderBy,Integer>(OrderBy.EMPTY_ORDER_BY, 0);
        }
        return new Pair<OrderBy,Integer>(new OrderBy(orderByExpressions), sourceRowKeyColumnOffset);
    }

    /**
     * For table with salted/multiTenant/viewIndexId,some leading rowkey columns should be skipped.
     * @param table
     * @param phoenixConnection
     * @return
     */
    public static int getRowKeyColumnOffset(PTable table, PhoenixConnection phoenixConnection) {
        boolean isSalted = table.getBucketNum() != null;
        boolean isMultiTenant = phoenixConnection.getTenantId() != null && table.isMultiTenant();
        boolean isSharedViewIndex = table.getViewIndexId() != null;
        return (isSalted ? 1 : 0) + (isMultiTenant ? 1 : 0) + (isSharedViewIndex ? 1 : 0);
    }

    /**
     * Create {@link RowKeyColumnExpression} from {@link PTable}.
     * The second part of the return pair is the rowkey column offset we must skip when we create OrderBys, because for table with salted/multiTenant/viewIndexId,
     * some leading rowkey columns should be skipped.
     * @param table
     * @param phoenixConnection
     * @return
     */
    public static Pair<List<RowKeyColumnExpression>,Integer> getRowKeyColumnExpressionsFromTable(PTable table, PhoenixConnection phoenixConnection) {
        int pkPositionOffset = getRowKeyColumnOffset(table, phoenixConnection);
        List<PColumn> pkColumns = table.getPKColumns();
        if(pkPositionOffset >= pkColumns.size()) {
            return new Pair<List<RowKeyColumnExpression>,Integer>(Collections.<RowKeyColumnExpression> emptyList(), 0);
        }
        List<RowKeyColumnExpression> rowKeyColumnExpressions = new ArrayList<RowKeyColumnExpression>(pkColumns.size() - pkPositionOffset);
        for(int index = pkPositionOffset; index < pkColumns.size(); index++) {
            RowKeyColumnExpression rowKeyColumnExpression =
                    new RowKeyColumnExpression(pkColumns.get(index), new RowKeyValueAccessor(pkColumns, index));
            rowKeyColumnExpressions.add(rowKeyColumnExpression);
        }
        return new Pair<List<RowKeyColumnExpression>,Integer>(rowKeyColumnExpressions, pkPositionOffset);
    }

    /**
     * Create OrderByExpression by RowKeyColumnExpression,isNullsLast is the default value "false",isAscending is based on {@link Expression#getSortOrder()}.
     * If orderByReverse is true, reverse the isNullsLast and isAscending.
     * @param rowKeyColumnExpressions
     * @param orderByReverse
     * @return
     */
    public static OrderBy convertRowKeyColumnExpressionsToOrderBy(List<RowKeyColumnExpression> rowKeyColumnExpressions, boolean orderByReverse) {
        return convertRowKeyColumnExpressionsToOrderBy(
                rowKeyColumnExpressions, Collections.<Info> emptyList(), orderByReverse);
    }

    /**
     * Create OrderByExpression by RowKeyColumnExpression, if the orderPreservingTrackInfos is not null, use isNullsLast and isAscending from orderPreservingTrackInfos.
     * If orderByReverse is true, reverse the isNullsLast and isAscending.
     * @param rowKeyColumnExpressions
     * @param orderPreservingTrackInfos
     * @param orderByReverse
     * @return
     */
    public static OrderBy convertRowKeyColumnExpressionsToOrderBy(
            List<RowKeyColumnExpression> rowKeyColumnExpressions,
            List<Info> orderPreservingTrackInfos,
            boolean orderByReverse) {
        if(rowKeyColumnExpressions.isEmpty()) {
            return OrderBy.EMPTY_ORDER_BY;
        }
        List<OrderByExpression> orderByExpressions = new ArrayList<OrderByExpression>(rowKeyColumnExpressions.size());
        Iterator<Info> orderPreservingTrackInfosIter = null;
        if(orderPreservingTrackInfos != null && orderPreservingTrackInfos.size() > 0) {
            if(orderPreservingTrackInfos.size() != rowKeyColumnExpressions.size()) {
                throw new IllegalStateException(
                        "orderPreservingTrackInfos.size():[" + orderPreservingTrackInfos.size() +
                        "] should equals rowKeyColumnExpressions.size():[" + rowKeyColumnExpressions.size()+"]!");
            }
            orderPreservingTrackInfosIter = orderPreservingTrackInfos.iterator();
        }
        for(RowKeyColumnExpression rowKeyColumnExpression : rowKeyColumnExpressions) {
            Info orderPreservingTrackInfo = null;
            if(orderPreservingTrackInfosIter != null) {
                assert orderPreservingTrackInfosIter.hasNext();
                orderPreservingTrackInfo = orderPreservingTrackInfosIter.next();
            }
            OrderByExpression orderByExpression =
                    OrderByExpression.convertExpressionToOrderByExpression(rowKeyColumnExpression, orderPreservingTrackInfo, orderByReverse);
            orderByExpressions.add(orderByExpression);
        }
        return new OrderBy(orderByExpressions);
    }

    /**
     * Convert the GroupBy to OrderBy, expressions in GroupBy should be converted to {@link RowKeyColumnExpression}.
     * @param groupBy
     * @param orderByReverse
     * @return
     */
    public static OrderBy convertGroupByToOrderBy(GroupBy groupBy, boolean orderByReverse) {
        if(groupBy.isEmpty()) {
            return OrderBy.EMPTY_ORDER_BY;
        }
        List<RowKeyColumnExpression> rowKeyColumnExpressions = convertGroupByToRowKeyColumnExpressions(groupBy);
        List<Info> orderPreservingTrackInfos = Collections.<Info> emptyList();
        if(groupBy.isOrderPreserving()) {
            orderPreservingTrackInfos = groupBy.getOrderPreservingTrackInfos();
        }
        return convertRowKeyColumnExpressionsToOrderBy(rowKeyColumnExpressions, orderPreservingTrackInfos, orderByReverse);
    }

    /**
     * Convert the expressions in GroupBy to {@link RowKeyColumnExpression}, the convert logic is same as {@link ExpressionCompiler#wrapGroupByExpression}.
     * @param groupBy
     * @return
     */
    public static List<RowKeyColumnExpression> convertGroupByToRowKeyColumnExpressions(GroupBy groupBy) {
        if(groupBy.isEmpty()) {
            return Collections.<RowKeyColumnExpression> emptyList();
        }
        List<Expression> groupByExpressions = groupBy.getExpressions();
        List<RowKeyColumnExpression> rowKeyColumnExpressions = new ArrayList<RowKeyColumnExpression>(groupByExpressions.size());
        int columnIndex = 0;
        for(Expression groupByExpression : groupByExpressions) {
            RowKeyColumnExpression rowKeyColumnExpression =
                    convertGroupByExpressionToRowKeyColumnExpression(groupBy, groupByExpression, columnIndex++);
            rowKeyColumnExpressions.add(rowKeyColumnExpression);
        }
        return rowKeyColumnExpressions;
    }

    /**
     * Convert the expressions in GroupBy to {@link RowKeyColumnExpression}, a typical case is in {@link ExpressionCompiler#wrapGroupByExpression}.
     * @param groupBy
     * @param originalExpression
     * @param groupByColumnIndex
     * @return
     */
    public static RowKeyColumnExpression convertGroupByExpressionToRowKeyColumnExpression(
            GroupBy groupBy,
            Expression originalExpression,
            int groupByColumnIndex) {
        RowKeyValueAccessor rowKeyValueAccessor = new RowKeyValueAccessor(groupBy.getKeyExpressions(), groupByColumnIndex);
        return new RowKeyColumnExpression(
                originalExpression,
                rowKeyValueAccessor,
                groupBy.getKeyExpressions().get(groupByColumnIndex).getDataType());
    }
}