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

import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;

import org.apache.cassandra.utils.Pair;

import static java.util.Collections.emptyMap;
import static java.util.Collections.unmodifiableMap;

/**
 * Manages the cached {@link TableMetadataRef} objects which holds the references to {@link TableMetadata} objects.
 * <p>
 * The purpose of {@link TableMetadataRef} is that the reference to {@link TableMetadataRef} remains unchanged when
 * the metadata of the table changes. {@link TableMetadata} is immutable, so when it changes, we only switch
 * the reference inside the existing {@link TableMetadataRef} object.
 */
class TableMetadataRefCache
{
    public final static TableMetadataRefCache EMPTY = new TableMetadataRefCache(emptyMap(), emptyMap(), emptyMap());

    // UUID -> mutable metadata ref map. We have to update these in place every time a table changes.
    private final Map<TableId, TableMetadataRef> metadataRefs;

    // keyspace and table names -> mutable metadata ref map.
    private final Map<Pair<String, String>, TableMetadataRef> metadataRefsByName;

    // (keyspace name, index name) -> mutable metadata ref map. We have to update these in place every time an index changes.
    private final Map<Pair<String, String>, TableMetadataRef> indexMetadataRefs;

    private TableMetadataRefCache(Map<TableId, TableMetadataRef> metadataRefs,
                                 Map<Pair<String, String>, TableMetadataRef> metadataRefsByName,
                                 Map<Pair<String, String>, TableMetadataRef> indexMetadataRefs)
    {
        this.metadataRefs = metadataRefs;
        this.metadataRefsByName = metadataRefsByName;
        this.indexMetadataRefs = indexMetadataRefs;
    }

    /**
     * Returns cache copy with added the {@link TableMetadataRef} corresponding to the provided keyspace to {@link #metadataRefs} and
     * {@link #indexMetadataRefs}, assuming the keyspace is new (in the sense of not being tracked by the manager yet).
     */
    TableMetadataRefCache withNewRefs(KeyspaceMetadata ksm)
    {
        return withUpdatedRefs(ksm.empty(), ksm);
    }

    /**
     * Returns cache copy with updated the {@link TableMetadataRef} in {@link #metadataRefs} and {@link #indexMetadataRefs},
     * for an existing updated keyspace given it's previous and new definition.
     * <p>
     * Note that {@link TableMetadataRef} are not duplicated and table metadata is altered in the existing refs.
     */
    TableMetadataRefCache withUpdatedRefs(KeyspaceMetadata previous, KeyspaceMetadata updated)
    {
        Tables.TablesDiff tablesDiff = Tables.diff(previous.tables, updated.tables);
        Views.ViewsDiff viewsDiff = Views.diff(previous.views, updated.views);

        MapDifference<String, TableMetadata> indexesDiff = previous.tables.indexesDiff(updated.tables);

        boolean hasCreatedOrDroppedTablesOrViews = tablesDiff.created.size() > 0 || tablesDiff.dropped.size() > 0 || viewsDiff.created.size() > 0 || viewsDiff.dropped.size() > 0;
        boolean hasCreatedOrDroppedIndexes = !indexesDiff.entriesOnlyOnRight().isEmpty() || !indexesDiff.entriesOnlyOnLeft().isEmpty();

        Map<TableId, TableMetadataRef> metadataRefs = hasCreatedOrDroppedTablesOrViews ? Maps.newHashMap(this.metadataRefs) : this.metadataRefs;
        Map<Pair<String, String>, TableMetadataRef> metadataRefsByName = hasCreatedOrDroppedTablesOrViews ? Maps.newHashMap(this.metadataRefsByName) : this.metadataRefsByName;
        Map<Pair<String, String>, TableMetadataRef> indexMetadataRefs = hasCreatedOrDroppedIndexes ? Maps.newHashMap(this.indexMetadataRefs) : this.indexMetadataRefs;

        // clean up after removed entries
        tablesDiff.dropped.forEach(ref -> removeRef(metadataRefs, metadataRefsByName, ref));
        viewsDiff.dropped.forEach(view -> removeRef(metadataRefs, metadataRefsByName, view.metadata));
        indexesDiff.entriesOnlyOnLeft()
                   .values()
                   .forEach(indexTable -> indexMetadataRefs.remove(Pair.create(indexTable.keyspace, indexTable.indexName().get())));

        // load up new entries
        tablesDiff.created.forEach(table -> putRef(metadataRefs, metadataRefsByName, new TableMetadataRef(table)));
        viewsDiff.created.forEach(view -> putRef(metadataRefs, metadataRefsByName, new TableMetadataRef(view.metadata)));
        indexesDiff.entriesOnlyOnRight()
                   .values()
                   .forEach(indexTable -> indexMetadataRefs.put(Pair.create(indexTable.keyspace, indexTable.indexName().get()), new TableMetadataRef(indexTable)));

        // refresh refs to updated ones
        tablesDiff.altered.forEach(diff -> metadataRefs.get(diff.after.id).set(diff.after));
        viewsDiff.altered.forEach(diff -> metadataRefs.get(diff.after.metadata.id).set(diff.after.metadata));
        indexesDiff.entriesDiffering()
                   .values()
                   .stream()
                   .map(MapDifference.ValueDifference::rightValue)
                   .forEach(indexTable -> indexMetadataRefs.get(Pair.create(indexTable.keyspace, indexTable.indexName().get())).set(indexTable));

        // Avoid re-wrapping the map if no changes
        return new TableMetadataRefCache(
            hasCreatedOrDroppedTablesOrViews ? unmodifiableMap(metadataRefs) : metadataRefs,
            hasCreatedOrDroppedTablesOrViews ? unmodifiableMap(metadataRefsByName) : metadataRefsByName,
            hasCreatedOrDroppedIndexes ? unmodifiableMap(indexMetadataRefs) : indexMetadataRefs);
    }

    private void putRef(Map<TableId, TableMetadataRef> metadataRefs,
                        Map<Pair<String, String>, TableMetadataRef> metadataRefsByName,
                        TableMetadataRef ref)
    {
        metadataRefs.put(ref.id, ref);
        metadataRefsByName.put(Pair.create(ref.keyspace, ref.name), ref);
    }

    private void removeRef(Map<TableId, TableMetadataRef> metadataRefs,
                           Map<Pair<String, String>, TableMetadataRef> metadataRefsByName,
                           TableMetadata tm)
    {
        metadataRefs.remove(tm.id);
        metadataRefsByName.remove(Pair.create(tm.keyspace, tm.name));
    }

    /**
     * Returns cache copy with removed the {@link TableMetadataRef} from {@link #metadataRefs} and {@link #indexMetadataRefs}
     * for the provided (dropped) keyspace.
     */
    TableMetadataRefCache withRemovedRefs(KeyspaceMetadata ksm)
    {
        return withUpdatedRefs(ksm, ksm.empty());
    }

    public TableMetadataRef getTableMetadataRef(TableId id)
    {
        return metadataRefs.get(id);
    }

    public TableMetadataRef getTableMetadataRef(String keyspace, String table)
    {
        return metadataRefsByName.get(Pair.create(keyspace, table));
    }

    public TableMetadataRef getIndexTableMetadataRef(String keyspace, String index)
    {
        return indexMetadataRefs.get(Pair.create(keyspace, index));
    }
}
