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

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Optional;

import javax.annotation.Nullable;

import org.antlr.runtime.RecognitionException;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;

import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.CqlBuilder;
import org.apache.cassandra.cql3.SchemaElement;
import org.apache.cassandra.cql3.WhereClause;
import org.apache.cassandra.cql3.functions.masking.ColumnMask;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.marshal.UserType;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.tcm.serialization.UDTAndFunctionsAwareMetadataSerializer;
import org.apache.cassandra.tcm.serialization.Version;

public final class ViewMetadata implements SchemaElement
{
    public static final Serializer serializer = new Serializer();

    public final TableId baseTableId;
    public final String baseTableName;

    public final boolean includeAllColumns;
    public final TableMetadata metadata;

    public final WhereClause whereClause;

    /**
     * @param baseTableId       Internal ID of the table which this view is based off of
     * @param includeAllColumns Whether to include all columns or not
     */
    public ViewMetadata(TableId baseTableId,
                        String baseTableName,
                        boolean includeAllColumns,
                        WhereClause whereClause,
                        TableMetadata metadata)
    {
        this.baseTableId = baseTableId;
        this.baseTableName = baseTableName;
        this.includeAllColumns = includeAllColumns;
        this.whereClause = whereClause;
        this.metadata = metadata;
    }

    public String keyspace()
    {
        return metadata.keyspace;
    }

    public String name()
    {
        return metadata.name;
    }

    /**
     * @return true if the view specified by this definition will include the column, false otherwise
     */
    public boolean includes(ColumnIdentifier column)
    {
        return metadata.getColumn(column) != null;
    }

    public ViewMetadata copy(TableMetadata newMetadata)
    {
        return new ViewMetadata(baseTableId, baseTableName, includeAllColumns, whereClause, newMetadata);
    }

    public TableMetadata baseTableMetadata()
    {
        return Schema.instance.getTableMetadata(baseTableId);
    }

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

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

        ViewMetadata other = (ViewMetadata) o;
        return baseTableId.equals(other.baseTableId)
            && includeAllColumns == other.includeAllColumns
            && whereClause.equals(other.whereClause)
            && metadata.equals(other.metadata);
    }

    Optional<Difference> compare(ViewMetadata other)
    {
        if (!baseTableId.equals(other.baseTableId) || includeAllColumns != other.includeAllColumns || !whereClause.equals(other.whereClause))
            return Optional.of(Difference.SHALLOW);

        return metadata.compare(other.metadata);
    }

    @Override
    public int hashCode()
    {
        return new HashCodeBuilder(29, 1597)
               .append(baseTableId)
               .append(includeAllColumns)
               .append(whereClause)
               .append(metadata)
               .toHashCode();
    }

    @Override
    public String toString()
    {
        return new ToStringBuilder(this)
               .append("baseTableId", baseTableId)
               .append("baseTableName", baseTableName)
               .append("includeAllColumns", includeAllColumns)
               .append("whereClause", whereClause)
               .append("metadata", metadata)
               .toString();
    }

    public boolean referencesUserType(ByteBuffer name)
    {
        return metadata.referencesUserType(name);
    }

    public ViewMetadata withUpdatedUserType(UserType udt)
    {
        return referencesUserType(udt.name)
             ? copy(metadata.withUpdatedUserType(udt))
             : this;
    }

    public ViewMetadata withRenamedPrimaryKeyColumn(ColumnIdentifier from, ColumnIdentifier to)
    {
        return new ViewMetadata(baseTableId,
                                baseTableName,
                                includeAllColumns,
                                whereClause.renameIdentifier(from, to),
                                metadata.unbuild().renamePrimaryKeyColumn(from, to).build());
    }

    public ViewMetadata withAddedRegularColumn(ColumnMetadata column)
    {
        return new ViewMetadata(baseTableId,
                                baseTableName,
                                includeAllColumns,
                                whereClause,
                                metadata.unbuild().addColumn(column).build());
    }

    public ViewMetadata withNewColumnMask(ColumnIdentifier name, @Nullable ColumnMask mask)
    {
        return new ViewMetadata(baseTableId,
                                baseTableName,
                                includeAllColumns,
                                whereClause,
                                metadata.unbuild().alterColumnMask(name, mask).build());
    }

    public void appendCqlTo(CqlBuilder builder,
                            boolean internals,
                            boolean ifNotExists)
    {
        builder.append("CREATE MATERIALIZED VIEW ");

        if (ifNotExists)
            builder.append("IF NOT EXISTS ");

        builder.append(metadata.toString())
               .append(" AS")
               .newLine()
               .increaseIndent()
               .append("SELECT ");

        if (includeAllColumns)
        {
            builder.append('*');
        }
        else
        {
            builder.appendWithSeparators(metadata.allColumnsInSelectOrder(), (b, c) -> b.append(c.name), ", ");
        }

        builder.newLine()
               .append("FROM ")
               .appendQuotingIfNeeded(metadata.keyspace)
               .append('.')
               .appendQuotingIfNeeded(baseTableName)
               .newLine()
               .append("WHERE ")
               .append(whereClause.toString())
               .newLine();

        metadata.appendPrimaryKey(builder);

        builder.decreaseIndent()
               .append(" WITH ")
               .increaseIndent();

        metadata.appendTableOptions(builder, internals);
    }

    @Override
    public SchemaElementType elementType()
    {
        return SchemaElementType.MATERIALIZED_VIEW;
    }

    @Override
    public String elementKeyspace()
    {
        return keyspace();
    }

    @Override
    public String elementName()
    {
        return name();
    }

    @Override
    public String toCqlString(boolean withWarnings, boolean withInternals, boolean ifNotExists)
    {
        CqlBuilder builder = new CqlBuilder(2048);
        appendCqlTo(builder, withInternals, ifNotExists);
        return builder.toString();
    }

    public static class Serializer implements UDTAndFunctionsAwareMetadataSerializer<ViewMetadata>
    {
        public void serialize(ViewMetadata t, DataOutputPlus out, Version version) throws IOException
        {
            TableMetadata.serializer.serialize(t.metadata, out, version);
            out.writeBoolean(t.includeAllColumns);
            t.baseTableId.serialize(out);
            out.writeUTF(t.whereClause.toCQLString());
            out.writeUTF(t.baseTableName);
        }

        public ViewMetadata deserialize(DataInputPlus in, Types types, UserFunctions functions, Version version) throws IOException
        {
            TableMetadata meta = TableMetadata.serializer.deserialize(in, types, functions, version);
            boolean includeAllColumns = in.readBoolean();
            TableId tableId = TableId.deserialize(in);
            String whereClause = in.readUTF();
            String baseTableName = in.readUTF();

            WhereClause wc;
            try
            {
                wc = WhereClause.parse(whereClause);
            }
            catch (RecognitionException e)
            {
                throw new RuntimeException(String.format("Unexpected error while parsing materialized view's where clause for '%s' (got %s)", meta.name, whereClause));
            }

            return new ViewMetadata(tableId, baseTableName, includeAllColumns, wc, meta);
        }

        public long serializedSize(ViewMetadata t, Version version)
        {
            long size = TableMetadata.serializer.serializedSize(t.metadata, version);
            size += TypeSizes.sizeof(t.includeAllColumns);
            size += t.baseTableId.serializedSize();
            size += TypeSizes.sizeof(t.whereClause.toCQLString());
            size += TypeSizes.sizeof(t.baseTableName);
            return size;
        }
    }
}
