/*
 * 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.cassandra.cql3;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;

import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.terms.Term;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.MapType;
import org.apache.cassandra.db.marshal.TupleType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;

import static org.apache.cassandra.cql3.statements.RequestValidations.checkContainsNoDuplicates;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkContainsOnly;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;

/**
 * An expression including one or several columns.
 *
 * <p>This class can be modified to add support for more column expressions like UDT fields, List elements,
 * functions on columns... </p>
 */
public final class ColumnsExpression
{
    /**
     * Represent the expression kind
     */
    public enum Kind
    {
        /**
         * Single column expression (e.g. {@code columnA})
         */
        SINGLE_COLUMN
        {
            @Override
            void validateColumns(TableMetadata table, List<ColumnMetadata> columns)
            {
            }

            @Override
            AbstractType<?> type(TableMetadata table, List<ColumnMetadata> columns, ElementExpression element)
            {
                return columns.get(0).type;
            }

            @Override
            String toCQLString(List<String> columns, String element)
            {
                return columns.get(0);
            }

            @Override
            public String toString()
            {
                return "single column";
            }
        },
        /**
         * Multi-column expression (e.g. {@code (columnA, columnB)})
         */
        MULTI_COLUMN
        {
            @Override
            protected void validateColumns(TableMetadata table, List<ColumnMetadata> columns)
            {
                int previousPosition = -1;
                for (int i = 0, m = columns.size(); i < m; i++)
                {
                    ColumnMetadata column = columns.get(i);
                    checkTrue(column.isClusteringColumn(), "Multi-column relations can only be applied to clustering columns but was applied to: %s", column.name);
                    checkFalse(columns.lastIndexOf(column) != i, "Column \"%s\" appeared twice in a relation: %s", column.name, this);

                    // check that no clustering columns were skipped
                    checkFalse(previousPosition != -1 && column.position() != previousPosition + 1,
                               "Clustering columns must appear in the PRIMARY KEY order in multi-column relations: %s", toCQLString(columns, null));

                    previousPosition = column.position();
                }
            }

            @Override
            AbstractType<?> type(TableMetadata table, List<ColumnMetadata> columns, ElementExpression element)
            {
                return new TupleType(ColumnMetadata.typesOf(columns));
            }

            @Override
            String toCQLString(List<String> columns, String element)
            {
                StringBuilder builder = new StringBuilder().append('(');
                Joiner.on(", ").appendTo(builder, columns);
                return builder.append(')').toString();
            }

            @Override
            public String toString()
            {
                return "multi-column";
            }
        },
        /**
         * Token expression (e.g. {@code token(columnA, columnB)})
         */
        TOKEN
        {
            @Override
            protected void validateColumns(TableMetadata table, List<ColumnMetadata> columns)
            {
                if (columns.equals(table.partitionKeyColumns()))
                    return;

                // If the columns do not match the partition key columns, let's try to narrow down the problem
                checkTrue(new HashSet<>(columns).containsAll(table.partitionKeyColumns()),
                        "The token() function must be applied to all partition key components or none of them");

                checkContainsNoDuplicates(columns, "The token() function contains duplicate partition key components");

                checkContainsOnly(columns, table.partitionKeyColumns(), "The token() function must contains only partition key components");

                throw invalidRequest("The token function arguments must be in the partition key order: %s",
                                     Joiner.on(", ").join(ColumnMetadata.toIdentifiers(table.partitionKeyColumns())));
            }

            @Override
            AbstractType<?> type(TableMetadata table, List<ColumnMetadata> columns, ElementExpression element)
            {
                return table.partitioner.getTokenValidator();
            }

            @Override
            String toCQLString(List<String> columns, String element)
            {
                StringBuilder builder = new StringBuilder();
                builder.append("token(");
                Joiner.on(", ").appendTo(builder, columns);
                return builder.append(')').toString();
            }

            @Override
            public String toString()
            {
                return "token";
            }
        },
        /**
         * Element expression (e.g. {@code columnA[?]}). This is used for collection elements and UDT fields. For more
         * information see {@link ElementExpression}.
         */
        ELEMENT
        {
            @Override
            void validateColumns(TableMetadata table, List<ColumnMetadata> columns)
            {
            }

            @Override
            AbstractType<?> type(TableMetadata table, List<ColumnMetadata> columns, ElementExpression element)
            {
                return element.type();
            }

            @Override
            String toCQLString(List<String> columns, String element)
            {
                return columns.get(0) + element;
            }
        };

        /**
         * Validates that the specified columns are valid for this kind of expression.
         * @param table the table metadata
         * @param columns the expression column
         */
        abstract void validateColumns(TableMetadata table, List<ColumnMetadata> columns);

        /**
         * Returns the expression type.
         *
         * @param table             the table metadata
         * @param columns           the expression columns
         * @param element           the element expression in case of ELEMENT columns expression
         * @return the expression type
         */
        abstract AbstractType<?> type(TableMetadata table, List<ColumnMetadata> columns, ElementExpression element);

        /**
         * Returns CQL representation of the expression.
         *
         * @param columns           the expression's columns
         * @param element           the element in case of ELEMENT columns expression
         * @return the CQL representation of the expression.
         */
        abstract String toCQLString(List<String> columns, String element);

        String toCQLString(List<ColumnMetadata> columns, ElementExpression elementExpression)
        {
            return toCQLString(ColumnMetadata.cqlNames(columns), elementExpression != null ? elementExpression.toCQLString() : "");
        }

        String toCQLString(List<ColumnIdentifier> identifiers, ElementExpression.Raw rawElement)
        {
            String element = rawElement == null ? "" : rawElement.toCQLString();
            return toCQLString(ColumnIdentifier.toCqlStrings(identifiers), element);
        }
    }

    private final Kind kind;

    /**
     * The type represented by this expression.
     * <li>
     *  <ul>for a single column the type of the expression will be the one of the column</ul>
     *  <ul>for a multi-column expression the type will be a tuple type</ul>
     *  <ul>for a token expression the type will be the token type</ul>
     *  <ul>for an element expression, the type will be one of the elements of interest(UDT field or collection element)</ul>
     * </li>
     */
    private final AbstractType<?> type;

    /**
     * The expression columns
     */
    private final List<ColumnMetadata> columns;

    private final TableMetadata table;

    /**
     * The element if this is an ELEMENT expression, {@code null} otherwise.
     * Like UDT field or collection element.
     */
    private final ElementExpression element; //Only relevant for ELEMENT kind

    ColumnsExpression(Kind kind, AbstractType<?> type, List<ColumnMetadata> columns, TableMetadata table, ElementExpression element)
    {
        assert kind != Kind.ELEMENT || element != null: "Element expression must have an element";
        this.kind = kind;
        this.type = type;
        this.columns = columns;
        this.table = table;
        this.element = element; // This could be null for kinds that don't use it
    }

    /**
     * Returns the expression type.
     * @return the expression type.
     */
    public AbstractType<?> type()
    {
        return type;
    }

    /**
     * Creates an expression for a single column (e.g. {@code columnA}).
     * @param column the column
     * @return an expression for a single column.
     */
    public static ColumnsExpression singleColumn(ColumnMetadata column, TableMetadata table)
    {
        return new ColumnsExpression(Kind.SINGLE_COLUMN, column.type, ImmutableList.of(column), table, null);
    }

    /**
     * Creates an expression for multi-columns (e.g. {@code (columnA, columnB)}).
     * @param columns the columns
     * @return an expression for multi-columns.
     */
    @VisibleForTesting
    public static ColumnsExpression multiColumns(List<ColumnMetadata> columns, TableMetadata table)
    {
        AbstractType<?> type = new TupleType(ColumnMetadata.types(columns));
        return new ColumnsExpression(Kind.MULTI_COLUMN, type, ImmutableList.copyOf(columns), table,null);
    }

    /**
     * Returns the first column metadata.
     * @return the first column metadata.
     */
    public ColumnMetadata firstColumn()
    {
        return columns().get(0);
    }

    public TableMetadata table()
    {
        return table;
    }

    /**
     * Returns the last column metadata.
     * @return the last column metadata.
     */
    public ColumnMetadata lastColumn()
    {
        return columns.get(columns.size() - 1);
    }

    /**
     * Returns the columns metadata in position order.
     * @return the columns metadata in position order.
     */
    public List<ColumnMetadata> columns()
    {
        return columns;
    }

    /**
     * Returns the column kind (partition key, clustering, static or regular).
     * @return the column kind.
     */
    public ColumnMetadata.Kind columnsKind()
    {
        // All columns must have the same kind.
        return firstColumn().kind;
    }

    /**
     * Returns the expression kind.
     * @return the expression kind.
     */
    public Kind kind()
    {
        return kind;
    }

    /**
     * Returns the key, index or fieldname specifying the selected element.
     * @return the key, index or fieldname specifying the selected element.
     */
    public ByteBuffer element(FunctionContext context)
    {
        return element.bindAndGet(context);
    }

    /**
     * Checks if this instance is a collection element expression.
     * @return {@code true} if this instance is a collection element expression, {@code false} otherwise.
     */
    public boolean isCollectionElementExpression()
    {
        return kind == Kind.ELEMENT && element != null && element.kind() == ElementExpression.Kind.COLLECTION_ELEMENT;
    }

    /**
     * Checks if this instance is a map element expression.
     * @return {@code true} if this instance is a map element expression, {@code false} otherwise.
     */
    public boolean isMapElementExpression()
    {
        return kind == Kind.ELEMENT && element != null && element.kind() == ElementExpression.Kind.COLLECTION_ELEMENT && firstColumn().type.unwrap() instanceof MapType;
    }

    /**
     * Collects the column specifications for the bind variables.
     * This is obviously a no-op if the expression is not a {@code ELEMENET_EXPRESSION} expression.
     *
     * @param boundNames the variables specification where to collect the
     * bind variables of the map key/collection element in.
     */
    public void collectMarkerSpecification(VariableSpecifications boundNames, Object owner)
    {
        if (element != null)
            element.collectMarkerSpecification(boundNames, owner);
    }

    /**
     * Checks if this instance is a column level expression (single or multi-column expression).
     * @return {@code true} if this instance is a column level expression, {@code false} otherwise.
     */
    public boolean isColumnLevelExpression()
    {
        return kind == Kind.SINGLE_COLUMN || kind == Kind.MULTI_COLUMN;
    }

    /**
     * Adds all functions (native and user-defined) used by any component of the restriction
     * to the specified list.
     * @param functions the list to add to
     */
    public void addFunctionsTo(List<Function> functions)
    {
        if (element != null)
            element.addFunctionsTo(functions);
    }

    /**
     * Returns CQL representation of this expression.
     * @return the CQL representation of this expression.
     */
    public String toCQLString()
    {
        return kind.toCQLString(columns, element);
    }

    @Override
    public String toString()
    {
        String prefix = kind == Kind.SINGLE_COLUMN ? "column "
                                                   : kind == Kind.MULTI_COLUMN ? "tuple " : "";

        return prefix + toCQLString();
    }

    /**
     * Returns the column specification corresponding to this expression.
     * @return the column specification corresponding to this expression.
     */
    public ColumnSpecification columnSpecification()
    {
        ColumnMetadata column = firstColumn();
        return kind == Kind.SINGLE_COLUMN ? column
                                          : new ColumnSpecification(column.ksName, column.cfName, new ColumnIdentifier(toCQLString(), true), type) ;
    }

    /**
     * The parsed version of the {@link ColumnsExpression} as outputed by the CQL parser.
     * {@code Raw.prepare} will be called upon schema binding to create the {@link ColumnsExpression}.
     */
    public static final class Raw
    {
        private final Kind kind;

        /**
         * The columns identifiers
         */
        private final List<ColumnIdentifier> identifiers;

        private final ElementExpression.Raw rawElement;

        private Raw(Kind kind, List<ColumnIdentifier> identifiers, ElementExpression.Raw rawElement)
        {
            this.kind = kind;
            this.identifiers = identifiers;
            this.rawElement = rawElement;
        }

        /**
         * Returns the expression kind.
         * @return the expression kind.
         */
        public Kind kind()
        {
            return kind;
        }

        /**
         * Creates a raw expression for a single column (e.g. {@code columnA}).
         * @param identifier the column identifier
         * @return a raw expression for a single column.
         */
        public static Raw singleColumn(ColumnIdentifier identifier)
        {
            return new Raw(Kind.SINGLE_COLUMN, ImmutableList.of(identifier), null);
        }

        /**
         * Creates a raw expression for multi-column (e.g. {@code (columnA, columnB)}).
         *
         * @param identifiers the columns identifier
         * @return a raw expression for multi-column.
         */
        public static Raw multiColumn(List<ColumnIdentifier> identifiers)
        {
            return new Raw(Kind.MULTI_COLUMN, identifiers, null);
        }

        /**
         * Creates a raw expression for token restrictions (e.g. {@code token(columnA, columnB)}).
         *
         * @param identifiers the columns identifiers
         * @return a raw token expression.
         */
        public static Raw token(List<ColumnIdentifier> identifiers)
        {
            return new Raw(Kind.TOKEN, identifiers, null);
        }

        /**
         * Creates a raw expression for collection element conditions (e.g. {@code columnA[?]}).
         *
         * @param identifier the collection element column identifier
         * @param rawCollectionElement the raw collection element
         * @return a raw element expression.
         */
        public static Raw collectionElement(ColumnIdentifier identifier, Term.Raw rawCollectionElement)
        {
            return new Raw(Kind.ELEMENT, ImmutableList.of(identifier), new ElementExpression.Raw(rawCollectionElement, null, ElementExpression.Kind.COLLECTION_ELEMENT));
        }

        /**
         * Creates a raw expression for a UDT field conditions.
         *
         * @param identifier the UDT field column identifier
         * @param rawUdtField the raw UDT field
         * @return a raw element expression.
         */
        public static Raw udtField(ColumnIdentifier identifier, FieldIdentifier rawUdtField)
        {
            return new Raw(Kind.ELEMENT, ImmutableList.of(identifier), new ElementExpression.Raw(null, rawUdtField, ElementExpression.Kind.UDT_FIELD));
        }

        /**
         * Renames an identifier in this expression, if applicable.
         *
         * @param from the old identifier
         * @param to the new identifier
         * @return this object, if the old identifier is not in the set of identifiers that this expression covers; otherwise
         *         a new Raw expression with "from" replaced by "to" is returned.
         */
        public Raw renameIdentifier(ColumnIdentifier from, ColumnIdentifier to)
        {
            if (!identifiers.contains(from))
                return this;

            List<ColumnIdentifier> newIdentifiers = identifiers.stream()
                                                               .map(e -> e.equals(from) ? to : e)
                                                               .collect(Collectors.toList());
            return new Raw(kind, newIdentifiers, rawElement);
        }

        /**
         * Checks if this raw expression contains bind markers.
         * @return {@code true} if this raw expression contains bind markers, {@code false} otherwise.
         */
        public boolean containsBindMarkers()
        {
            return rawElement != null && rawElement.containsBindMarkers();
        }

        /**
         * Bind this {@link Raw} instance to the schema and return the resulting {@link ColumnsExpression}.
         *
         * @param table the table schema
         * @return the {@link ColumnsExpression} resulting from the schema binding
         */
        public ColumnsExpression prepare(TableMetadata table)
        {
            List<ColumnMetadata> columns = getColumnsMetadata(table, identifiers);
            kind.validateColumns(table, columns);

            ElementExpression elementExpression = null;
            if (kind == Kind.ELEMENT)
                elementExpression = rawElement.prepare(columns.get(0));

            AbstractType<?> type = kind.type(table, columns, elementExpression);

            return new ColumnsExpression(kind, type, columns, table, elementExpression);
        }

        /**
         * Returns the columns corresponding to the identifiers.
         *
         * @param table the table metadata
         * @param identifiers the columns identifiers
         * @return the definition of the columns to which apply the token restriction.
         * @throws InvalidRequestException if the entity cannot be resolved
         */
        private static List<ColumnMetadata> getColumnsMetadata(TableMetadata table, List<ColumnIdentifier> identifiers)
        {
            List<ColumnMetadata> columns = new ArrayList<>(identifiers.size());
            for (ColumnIdentifier id : identifiers)
                columns.add(table.getExistingColumn(id));
            return columns;
        }

        /**
         * Returns the columns' identifiers.
         *
         * @return identifiers.
         */
        public List<ColumnIdentifier> identifiers()
        {
            return identifiers;
        }

        @Override
        public int hashCode()
        {
            return Objects.hash(kind, identifiers, rawElement);
        }

        @Override
        public boolean equals(Object o)
        {
            if (this == o)
                return true;

            if (!(o instanceof Raw))
                return false;

            Raw r = (Raw) o;
            return kind == r.kind && Objects.equals(identifiers, r.identifiers) && Objects.equals(rawElement, r.rawElement);
        }

        /**
         * Returns CQL representation of this raw expression.
         *
         * @return the CQL representation of this raw expression.
         */
        public String toCQLString()
        {
            return kind.toCQLString(identifiers, rawElement);
        }
    }
}
