/**
 * 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.camel.component.gridfs;

import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.Mongo;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.gridfs.GridFS;
import org.apache.camel.Consumer;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.impl.DefaultEndpoint;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.UriEndpoint;
import org.apache.camel.spi.UriParam;
import org.apache.camel.spi.UriPath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@UriEndpoint(scheme = "gridfs", title = "MongoDBGridFS", syntax = "gridfs:connectionBean", 
            label = "database,nosql")
public class GridFsEndpoint extends DefaultEndpoint {
    
    public enum QueryStrategy {
        TimeStamp,
        PersistentTimestamp,
        FileAttribute,
        TimeStampAndFileAttribute,
        PersistentTimestampAndFileAttribute
    };
    public static final String GRIDFS_OPERATION = "gridfs.operation";
    public static final String GRIDFS_METADATA = "gridfs.metadata";
    public static final String GRIDFS_CHUNKSIZE = "gridfs.chunksize";
    public static final String GRIDFS_FILE_ID_PRODUCED = "gridfs.fileid";

    private static final Logger LOG = LoggerFactory.getLogger(GridFsEndpoint.class);

    @UriPath @Metadata(required = "true")
    private String connectionBean;
    @UriParam @Metadata(required = "true")
    private String database;
    @UriParam(defaultValue = GridFS.DEFAULT_BUCKET)
    private String bucket;
    @UriParam(enums = "ACKNOWLEDGED,W1,W2,W3,UNACKNOWLEDGED,JOURNALED,MAJORITY,SAFE")
    private WriteConcern writeConcern;
    @UriParam
    private WriteConcern writeConcernRef;
    @UriParam
    private ReadPreference readPreference;
    
    @UriParam(label = "producer")
    private String operation;

    @UriParam(label = "consumer")
    private String query;
    @UriParam(label = "consumer", defaultValue = "1000")
    private long initialDelay = 1000;
    @UriParam(label = "consumer", defaultValue = "500")
    private long delay = 500;
    
    @UriParam(label = "consumer", defaultValue = "TimeStamp")
    private QueryStrategy queryStrategy = QueryStrategy.TimeStamp;
    @UriParam(label = "consumer", defaultValue = "camel-timestamps")
    private String persistentTSCollection = "camel-timestamps";
    @UriParam(label = "consumer", defaultValue = "camel-timestamp")
    private String persistentTSObject = "camel-timestamp";
    @UriParam(label = "consumer", defaultValue = "camel-processed")
    private String fileAttributeName = "camel-processed";


    private Mongo mongoConnection;
    private DB db;
    private GridFS gridFs;
    private DBCollection filesCollection;

    public GridFsEndpoint(String uri, GridFsComponent component) {
        super(uri, component);
    }

    @Override
    public Producer createProducer() throws Exception {
        initializeConnection();
        return new GridFsProducer(this);
    }

    @Override
    public Consumer createConsumer(Processor processor) throws Exception {
        initializeConnection();
        return new GridFsConsumer(this, processor);
    }

    public boolean isSingleton() {
        return true;
    }

    @SuppressWarnings("deprecation")
    public void initializeConnection() throws Exception {
        LOG.info("Initialize GridFS endpoint: {}", this.toString());
        if (database == null) {
            throw new IllegalStateException("Missing required endpoint configuration: database");
        }
        db = mongoConnection.getDB(database);
        if (db == null) {
            throw new IllegalStateException("Could not initialize GridFsComponent. Database " + database + " does not exist.");
        }
        gridFs = new GridFS(db, bucket == null ? GridFS.DEFAULT_BUCKET : bucket) {
            {
                filesCollection = getFilesCollection();
            }
        };
    }

    
    @Override
    protected void doStart() throws Exception {
        if (writeConcern != null && writeConcernRef != null) {
            String msg = "Cannot set both writeConcern and writeConcernRef at the same time. Respective values: " + writeConcern
                    + ", " + writeConcernRef + ". Aborting initialization.";
            throw new IllegalArgumentException(msg);
        }

        setWriteReadOptionsOnConnection();
        super.doStart();
    }
    private void setWriteReadOptionsOnConnection() {
        // Set the WriteConcern
        if (writeConcern != null) {
            mongoConnection.setWriteConcern(writeConcern);
        } else if (writeConcernRef != null) {
            mongoConnection.setWriteConcern(writeConcernRef);
        }

        // Set the ReadPreference
        if (readPreference != null) {
            mongoConnection.setReadPreference(readPreference);
        }
    }
    
    
    
    
    // ======= Getters and setters ===============================================
    public String getConnectionBean() {
        return connectionBean;
    }
    /**
     * Name of {@link com.mongodb.Mongo} to use.
     */
    public void setConnectionBean(String connectionBean) {
        this.connectionBean = connectionBean;
    }
    
    public Mongo getMongoConnection() {
        return mongoConnection;
    }
    /**
     * Sets the Mongo instance that represents the backing connection
     * 
     * @param mongoConnection the connection to the database
     */
    public void setMongoConnection(Mongo mongoConnection) {
        this.mongoConnection = mongoConnection;
    }

    public DB getDB() {
        return db;
    }
    
    public String getDatabase() {
        return database;
    }
    /**
     * Sets the name of the MongoDB database to target
     * 
     * @param database name of the MongoDB database
     */
    public void setDatabase(String database) {
        this.database = database;
    }
    /**
     * Sets the name of the GridFS bucket within the database.   Default is "fs".
     * 
     * @param database name of the MongoDB database
     */
    public String getBucket() {
        return bucket;
    }
    public void setBucket(String bucket) {
        this.bucket = bucket;
    }
    
    public String getQuery() {
        return query;
    }
    /**
     * Additional query parameters (in JSON) that are used to configure the query used for finding
     * files in the GridFsConsumer
     * @param query
     */
    public void setQuery(String query) {
        this.query = query;
    }
    public long getDelay() {
        return delay;
    }
    /**
     * Sets the delay between polls within the Consumer.  Default is 500ms
     * @param delay
     */
    public void setDelay(long delay) {
        this.delay = delay;
    }
    public long getInitialDelay() {
        return initialDelay;
    }
    /**
     * Sets the initialDelay before the consumer will start polling.  Default is 1000ms
     * @param initialDelay
     */
    public void setInitialDelay(long initialDelay) {
        this.initialDelay = delay;
    }
    
    /**
     * Sets the QueryStrategy that is used for polling for new files.  Default is Timestamp
     * @see QueryStrategy
     * @param s
     */
    public void setQueryStrategy(String s) {
        queryStrategy = QueryStrategy.valueOf(s);
    }
    public QueryStrategy getQueryStrategy() {
        return queryStrategy;
    }
    /**
     * If the QueryType uses a persistent timestamp, this sets the name of the collection within
     * the DB to store the timestamp.
     * @param s
     */
    public void setPersistentTSCollection(String s) {
        persistentTSCollection = s;
    }
    public String getPersistentTSCollection() {
        return persistentTSCollection;
    }
    /**
     * If the QueryType uses a persistent timestamp, this is the ID of the object in the collection
     * to store the timestamp.   
     * @param s
     */
    public void setPersistentTSObject(String id) {
        persistentTSObject = id;
    }
    public String getPersistentTSObject() {
        return persistentTSObject;
    }
    
    /**
     * If the QueryType uses a FileAttribute, this sets the name of the attribute that is used. Default is "camel-processed".
     * @param f
     */
    public void setFileAttributeName(String f) {
        fileAttributeName = f;
    }
    public String getFileAttributeName() {
        return fileAttributeName;
    }   
    
    /**
     * Set the {@link WriteConcern} for write operations on MongoDB using the standard ones.
     * Resolved from the fields of the WriteConcern class by calling the {@link WriteConcern#valueOf(String)} method.
     * 
     * @param writeConcern the standard name of the WriteConcern
     * @see <a href="http://api.mongodb.org/java/current/com/mongodb/WriteConcern.html#valueOf(java.lang.String)">possible options</a>
     */
    public void setWriteConcern(String writeConcern) {
        this.writeConcern = WriteConcern.valueOf(writeConcern);
    }

    public WriteConcern getWriteConcern() {
        return writeConcern;
    }

    /**
     * Set the {@link WriteConcern} for write operations on MongoDB, passing in the bean ref to a custom WriteConcern which exists in the Registry.
     * You can also use standard WriteConcerns by passing in their key. See the {@link #setWriteConcern(String) setWriteConcern} method.
     * 
     * @param writeConcernRef the name of the bean in the registry that represents the WriteConcern to use
     */
    public void setWriteConcernRef(String writeConcernRef) {
        WriteConcern wc = this.getCamelContext().getRegistry().lookupByNameAndType(writeConcernRef, WriteConcern.class);
        if (wc == null) {
            String msg = "Camel MongoDB component could not find the WriteConcern in the Registry. Verify that the "
                    + "provided bean name (" + writeConcernRef + ")  is correct. Aborting initialization.";
            throw new IllegalArgumentException(msg);
        }

        this.writeConcernRef = wc;
    }

    public WriteConcern getWriteConcernRef() {
        return writeConcernRef;
    }

    /** 
     * Sets a MongoDB {@link ReadPreference} on the Mongo connection. Read preferences set directly on the connection will be
     * overridden by this setting.
     * <p/>
     * The {@link com.mongodb.ReadPreference#valueOf(String)} utility method is used to resolve the passed {@code readPreference}
     * value. Some examples for the possible values are {@code nearest}, {@code primary} or {@code secondary} etc.
     * 
     * @param readPreference the name of the read preference to set
     */
    public void setReadPreference(String readPreference) {
        this.readPreference = ReadPreference.valueOf(readPreference);
    }

    public ReadPreference getReadPreference() {
        return readPreference;
    }
    
    
    /**
     * Sets the operation this endpoint will execute against GridRS.
     */
    public void setOperation(String operation) {
        this.operation = operation;
    }

    public String getOperation() {
        return operation;
    }

    public GridFS getGridFs() {
        return gridFs;
    }

    public void setGridFs(GridFS gridFs) {
        this.gridFs = gridFs;
    }
    public DBCollection getFilesCollection() {
        return filesCollection;
    }

}
