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

import static java.util.Objects.requireNonNull;

import java.util.List;
import java.util.Properties;

import org.apache.accumulo.core.security.Authorizations;
import org.apache.hadoop.conf.Configuration;
import org.apache.rya.api.RdfCloudTripleStoreConfiguration;
import org.apache.rya.mongodb.aggregation.AggregationPipelineQueryOptimizer;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryOptimizer;

import edu.umd.cs.findbugs.annotations.Nullable;

/**
 * A {@link RdfCloudTripleStoreConfiguration} that configures how Rya connects to a MongoDB Rya triple store.
 */
public class MongoDBRdfConfiguration extends RdfCloudTripleStoreConfiguration {

    // MongoDB Server connection values.
    public static final String MONGO_HOSTNAME = "mongo.db.instance";
    public static final String MONGO_PORT = "mongo.db.port";

    // MongoDB Database values.
    public static final String MONGO_DB_NAME = "mongo.db.name";
    public static final String MONGO_USER = "mongo.db.user";
    public static final String MONGO_USER_PASSWORD = "mongo.db.userpassword";

    // Rya Instance values.
    public static final String MONGO_COLLECTION_PREFIX = "mongo.db.collectionprefix";

    // Rya Sail configuration values.
    public static final String USE_MOCK_MONGO = ".useMockInstance";
    public static final String CONF_FLUSH_EACH_UPDATE = "rya.mongodb.dao.flusheachupdate";
    public static final String CONF_ADDITIONAL_INDEXERS = "ac.additional.indexers";
    public static final String MONGO_GEO_MAXDISTANCE = "mongo.geo.maxdist";

    public static final String USE_AGGREGATION_PIPELINE = "rya.mongodb.query.pipeline";

    /**
     * Constructs an empty instance of {@link MongoDBRdfConfiguration}.
     */
    public MongoDBRdfConfiguration() {
        super();
    }

    /**
     * Constructs an instance of {@link MongoDBRdfConfiguration} pre-loaded with values.
     *
     * @param other - The values that will be cloned into the constructed object. (not null)
     */
    public MongoDBRdfConfiguration(final Configuration other) {
        super( requireNonNull(other) );
    }

    /**
     * Reads a {@link Properties} object into a {@link MongoDBRdfConfiguration}.
     * See {@link MongoDBRdfConfigurationBuilder#fromProperties(Properties)} for which keys
     * are to be used within the properties object. This method will replace that object's keys
     * with the configuration object's keys since they are not the same.
     *
     * @param props - The properties containing Mongo specific configuration parameters. (not null)
     * @return A {@link } loaded with the values that were in {@code props}.
     */
    public static MongoDBRdfConfiguration fromProperties(final Properties props) {
        requireNonNull(props);
        return MongoDBRdfConfigurationBuilder.fromProperties(props);
    }

    /**
     * @return A new instance of {@link MongoDBRdfConfigurationBuilder}.
     */
    public static MongoDBRdfConfigurationBuilder getBuilder() {
        return new MongoDBRdfConfigurationBuilder();
    }

    @Override
    public MongoDBRdfConfiguration clone() {
        return new MongoDBRdfConfiguration(this);
    }

    /**
     * Set whether the Rya client should spin up an embedded MongoDB instance and connect to that
     * or if it should connect to a MongoDB Server that is running somewhere.
     *
     * @param useMock - {@code true} to use an embedded Mongo DB instance; {@code false} to connect to a real server.
     */
    public void setUseMock(final boolean useMock) {
        this.setBoolean(USE_MOCK_MONGO, useMock);
    }

    /**
     * Indicates whether the Rya client should spin up an embedded MongoDB instance and connect to that
     * or if it should connect to a MongoDB Server that is running somewhere.
     *
     * @return {@code true} to use an embedded Mongo DB instance; {@code false} to connect to a real server.
     */
    public boolean getUseMock() {
        return getBoolean(USE_MOCK_MONGO, false);
    }

    /**
     * @return The hostname of the MongoDB Server to connect to. (default: localhost)
     */
    public String getMongoHostname() {
        return get(MONGO_HOSTNAME, "localhost");
    }

    /**
     * @param hostname - The hostname of the MongoDB Server to connect to.
     */
    public void setMongoHostname(final String hostname) {
        requireNonNull(hostname);
        set(MONGO_HOSTNAME, hostname);
    }

    /**
     * @return The port of the MongoDB Server to connect to. (default: 27017)
     */
    public String getMongoPort() {
        return get(MONGO_PORT, AbstractMongoDBRdfConfigurationBuilder.DEFAULT_MONGO_PORT);
    }

    /**
     * @param port - The port of the MongoDB Server to connect to.
     */
    public void setMongoPort(final String port) {
        requireNonNull(port);
        set(MONGO_PORT, port);
    }

    /**
     * @return The name of the MongoDB Database to connect to. (default: rya)
     */
    public String getMongoDBName() {
        return get(MONGO_DB_NAME, "rya");
    }

    /**
     * @param database - The name of the MongoDb Database to connect to.
     */
    public void setMongoDBName(final String database) {
        requireNonNull(database);
        set(MONGO_DB_NAME, database);
    }

    /**
     * @param user - The user used to connect to the MongoDB Database that hosts the Rya Instance. (not null)
     */
    public void setMongoUser(final String user) {
        requireNonNull(user);
        set(MONGO_USER, user);
    }

    /**
     * @return The user used to connect to the MongoDB Database that hosts the Rya Instance.
     */
    public @Nullable String getMongoUser() {
        return get(MONGO_USER);
    }

    /**
     * @param password - The password used to connect to the MongoDB Database that hosts the Rya Instance.
     */
    public void setMongoPassword(final String password) {
        requireNonNull(password);
        set(MONGO_USER_PASSWORD, password);
    }

    /**
     * @return The password used to connect to the MongoDB Database that hosts the Rya Instance.
     */
    public @Nullable String getMongoPassword() {
        return get(MONGO_USER_PASSWORD);
    }

    /**
     * @return The name of the Rya instance to connect to. (default: rya)
     */
    public String getRyaInstanceName() {
        return get(MONGO_COLLECTION_PREFIX, "rya");
    }

    /**
     * @param name - The name of the Rya instance to connect to.
     */
    public void setRyaInstanceName(final String name) {
        requireNonNull(name);
        set(MONGO_COLLECTION_PREFIX, name);
    }

    /**
     * @return The name of the MongoDB Collection that contains Rya statements. (default: rya_triples)
     */
    public String getTriplesCollectionName() {
        return getRyaInstanceName() + "_triples";
    }

    /**
     * @return The name of the MongoDB Collection that contains the Rya namespace. (default: rya_ns)
     */
    public String getNameSpacesCollectionName() {
        return getRyaInstanceName() + "_ns";
    }

    /**
     * @return The authorizations that will be used when accessing data. (default: empty)
     */
    public Authorizations getAuthorizations() {
        final String[] auths = getAuths();
        if (auths == null || auths.length == 0) {
            return MongoDbRdfConstants.ALL_AUTHORIZATIONS;
        }
        return new Authorizations(auths);
    }

    /**
     * Indicates whether each statement added to the batch writer should be flushed and written
     * right away to the datastore or not. If this is turned off, then the statements will be
     * queued and written to the datastore when the queue is full or after enough time has
     * passed without a write.
     *
     * @return {@code true} if flushing after each updated is enabled; otherwise {@code false}. (default: true)
     */
    public boolean flushEachUpdate(){
        return getBoolean(CONF_FLUSH_EACH_UPDATE, true);
    }

    /**
     * Set whether each statement added to the batch writer should be flushed and written
     * right away to the datastore or not. If this is turned off, then the statements will be
     * queued and written to the datastore when the queue is full or after enough time has
     * passed without a write.
     *
     * @param flush - {@code true} if flushing after each updated is enabled; otherwise {@code false}.
     */
    public void setFlush(final boolean flush){
        setBoolean(CONF_FLUSH_EACH_UPDATE, flush);
    }

    /**
     * Whether aggregation pipeline optimization is enabled.
     * @return true if queries will be evaluated using MongoDB aggregation.
     */
    public boolean getUseAggregationPipeline() {
        return getBoolean(USE_AGGREGATION_PIPELINE, false);
    }

    /**
     * Enable or disable an optimization that executes queries, to the extent
     * possible, using the MongoDB aggregation pipeline. Replaces a query tree
     * or subtree with a single node representing a series of pipeline steps.
     * Transformation may not be supported for all query algebra expressions;
     * these expressions are left unchanged and the optimization is attempted
     * on their child subtrees.
     * @param value whether to use aggregation pipeline optimization.
     */
    public void setUseAggregationPipeline(final boolean value) {
        setBoolean(USE_AGGREGATION_PIPELINE, value);
    }

    @Override
    public List<Class<QueryOptimizer>> getOptimizers() {
        final List<Class<QueryOptimizer>> optimizers = super.getOptimizers();
        if (getUseAggregationPipeline()) {
            final Class<?> cl = AggregationPipelineQueryOptimizer.class;
            @SuppressWarnings("unchecked")
            final Class<QueryOptimizer> optCl = (Class<QueryOptimizer>) cl;
            optimizers.add(optCl);
        }
        return optimizers;
    }
}
