/*
 * Licensed to Luca Cavanna (the "Author") under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. Elastic Search 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.elasticsearch.shell.client;

import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.common.base.Predicate;
import org.elasticsearch.common.collect.Sets;
import org.elasticsearch.index.mapper.MapperService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.Set;

/**
 * @author Luca Cavanna
 *
 * Runnable that keeps up-to-date the shell scope while the indexes and types available in elasticsearch change.
 * Needed in order to provide the ability to run commands that are client or type specific (e.g. client.index.type.search() )
 */
public abstract class ClientScopeSynchronizer<ShellNativeClient> implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(ClientScopeSynchronizer.class);

    protected final ShellNativeClient shellNativeClient;
    protected Set<Index> indexes = new HashSet<Index>();

    protected ClientScopeSynchronizer(ShellNativeClient shellNativeClient) {
        this.shellNativeClient = shellNativeClient;
    }

    /**
     * Keeps in sync the shell scope
     */
    @Override
    public void run() {
        try {
            syncIndexes(getIndexes());
        } catch(Throwable t) {
            logger.info("Error while synchronizing the scope", t);
        }
    }

    /**
     * Retrieves the current indexes and types from elasticsearch
     * @return a set containing the indexes available in the elasticsearch cluster and their types
     */
    protected Set<Index> getIndexes() {
        ClusterStateResponse response = unwrapShellNativeClient().client().admin().cluster().prepareState().setFilterBlocks(true)
                .setFilterRoutingTable(true).setFilterNodes(true).execute().actionGet();

        Set<Index> newIndexes = new HashSet<Index>();
        for (IndexMetaData indexMetaData : response.getState().metaData().indices().values()) {
            logger.trace("Processing index {}", indexMetaData.index());

            Set<String> typeNames = Sets.filter(indexMetaData.mappings().keySet(), new Predicate<String>() {
                @Override
                public boolean apply(String s) {
                    return !MapperService.DEFAULT_MAPPING.equals(s);
                }
            });
            String[] types = typeNames.toArray(new String[typeNames.size()]);

            newIndexes.add(new Index(indexMetaData.index(), false, types));

            for (String alias : indexMetaData.aliases().keySet()) {
                newIndexes.add(new Index(alias, true, types));
            }
        }
        return newIndexes;
    }

    protected abstract AbstractClient unwrapShellNativeClient();

    /**
     * Synchronizes the registered indexes given the new indexes retrieved from the elasticsearch cluster
     * @param newIndexes the indexes currently available in the cluster
     */
    protected synchronized void syncIndexes(Set<Index> newIndexes) {
        //every index that is currently available gets registered (insert/update)
        for (Index index : newIndexes) {
            registerIndex(index);
            indexes.remove(index);
        }

        //The indexes that are left in the set need to be removed because they don't exist anymore
        for (Index index : indexes) {
            unregisterIndex(index);
        }

        this.indexes = newIndexes;
    }

    /**
     * Registers an index to the shell scope
     * @param index the index that needs to be registered to the shell scope
     */
    @SuppressWarnings("unchecked")
    protected void registerIndex(Index index) {
        InternalIndexClient indexClient = new InternalIndexClient(unwrapShellNativeClient(), index.name(), index.isAlias());
        InternalTypeClient[] typeClients = new InternalTypeClient[index.types().length];
        if (index.types() != null) {
            for (int i = 0; i < index.types().length; i++) {
                typeClients[i] = new InternalTypeClient(unwrapShellNativeClient(), index.name(), index.types()[i]);
            }
        }
        registerIndexAndTypes(indexClient, typeClients);
    }

    /**
     * Registers the {@link InternalIndexClient} and related {@link InternalTypeClient} for each available type
     * @param indexClient clients that exposes only the index-specific operations
     * @param typeClients list of clients that expose only the type-specific operations
     */
    protected abstract void registerIndexAndTypes(InternalIndexClient indexClient, InternalTypeClient... typeClients);

    /**
     * Unregisters an index from the shell scope
     * @param index the index that needs to be unregistered
     */
    protected abstract void unregisterIndex(Index index);

    /**
     * Inner class that represents an index with its optional types
     */
    static class Index {

        private final String name;
        private final String[] types;
        private final boolean alias;

        Index(String name, boolean alias, String... types) {
            this.name = name;
            this.types = types;
            this.alias = alias;
        }

        public String name() {
            return name;
        }

        public String[] types() {
            return types;
        }

        public boolean isAlias() {
            return alias;
        }

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

            Index index = (Index) o;

            return !(name != null ? !name.equals(index.name) : index.name != null);

        }

        @Override
        public int hashCode() {
            return name != null ? name.hashCode() : 0;
        }
    }
}