/*
 * Copyright 2011-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.s3.transfer;

import com.amazonaws.annotation.NotThreadSafe;
import com.amazonaws.annotation.SdkTestInternalApi;
import com.amazonaws.client.builder.ExecutorFactory;
import com.amazonaws.internal.SdkFunction;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.transfer.internal.TransferManagerUtils;

import java.util.concurrent.ExecutorService;

/**
 * Fluent builder for {@link TransferManager}. Use of the builder is preferred over constructors in
 * the TransferManager class.
 **/
@NotThreadSafe
public final class TransferManagerBuilder {

    private static final SdkFunction<TransferManagerParams, TransferManager> DEFAULT_TRANSFER_MANAGER_FACTORY = new SdkFunction<TransferManagerParams, TransferManager>() {
        @Override
        public TransferManager apply(TransferManagerParams params) {
            return new TransferManager(params);
        }
    };

    private final SdkFunction<TransferManagerParams, TransferManager> transferManagerFactory;

    private AmazonS3 s3Client;

    private ExecutorFactory executorFactory;

    private Boolean shutDownThreadPools;

    private Long minimumUploadPartSize;

    private Long multipartUploadThreshold;

    private Long multipartCopyThreshold;

    private Long multipartCopyPartSize;

    private Boolean disableParallelDownloads;

    /**
     * @return Create new instance of builder with all defaults set.
     */
    public static TransferManagerBuilder standard() {
        return new TransferManagerBuilder();
    }

    /**
     * @return Default TransferManager with default configuration. Uses {@link
     * AmazonS3ClientBuilder} to create a default {@link AmazonS3} client implementation.
     */
    public static TransferManager defaultTransferManager() {
        return standard().build();
    }

    private TransferManagerBuilder() {
        this(DEFAULT_TRANSFER_MANAGER_FACTORY);
    }

    @SdkTestInternalApi
    TransferManagerBuilder(
            SdkFunction<TransferManagerParams, TransferManager> transferManagerFactory) {
        this.transferManagerFactory = transferManagerFactory;
    }

    /**
     * @return The low level client currently configured in the builder.
     */
    public final AmazonS3 getS3Client() {
        return s3Client;
    }

    /**
     * Sets the low level client used to make the service calls to Amazon S3.
     *
     * @param s3Client Client implementation to use
     */
    public final void setS3Client(AmazonS3 s3Client) {
        this.s3Client = s3Client;
    }

    /**
     * Sets the low level client used to make the service calls to Amazon S3.
     *
     * @param s3Client Client implementation to use
     * @return This object for method chaining.
     */
    public final TransferManagerBuilder withS3Client(AmazonS3 s3Client) {
        setS3Client(s3Client);
        return this;
    }

    private AmazonS3 resolveS3Client() {
        return s3Client == null ? AmazonS3ClientBuilder.defaultClient() : s3Client;
    }

    /**
     * @return The {@link ExecutorFactory} currently configured in the builder.
     */
    public final ExecutorFactory getExecutorFactory() {
        return executorFactory;
    }

    /**
     * Sets a new {@link ExecutorFactory} for the builder. The factory is invoked for each transfer
     * manager created through the builder.
     *
     * @param executorFactory New executor factory to use.
     */
    public final void setExecutorFactory(ExecutorFactory executorFactory) {
        this.executorFactory = executorFactory;
    }

    /**
     * Sets a new {@link ExecutorFactory} for the builder. The factory is invoked for each transfer
     * manager created through the builder.
     *
     * @param executorFactory New executor factory to use.
     * @return This object for method chaining.
     */
    public final TransferManagerBuilder withExecutorFactory(ExecutorFactory executorFactory) {
        setExecutorFactory(executorFactory);
        return this;
    }

    private ExecutorService resolveExecutorService() {
        return executorFactory == null ? TransferManagerUtils.createDefaultExecutorService() :
                executorFactory.newExecutor();
    }

    /**
     * @return Current configured option on how thread pools should be managed when transfer manager
     * is shut down.
     */
    public final Boolean isShutDownThreadPools() {
        return shutDownThreadPools;
    }

    /**
     * By default, when the transfer manager is shut down, the underlying {@link ExecutorService} is
     * also shut down. For cases where the same thread pool is reused across different workloads you
     * can set this option to false to disable that behavior.
     *
     * @param shutDownThreadPools True to shut down thread pools on transfer manager shutdown, false
     *                            otherwise.
     */
    public final void setShutDownThreadPools(Boolean shutDownThreadPools) {
        this.shutDownThreadPools = shutDownThreadPools;
    }

    /**
     * By default, when the transfer manager is shut down, the underlying {@link ExecutorService} is
     * also shut down. For cases where the same thread pool is reused across different workloads you
     * can set this option to false to disable that behavior.
     *
     * @param shutDownThreadPools True to shut down thread pools on transfer manager shutdown, false
     *                            otherwise.
     * @return This object for method chaining.
     */
    public final TransferManagerBuilder withShutDownThreadPools(Boolean shutDownThreadPools) {
        setShutDownThreadPools(shutDownThreadPools);
        return this;
    }

    private Boolean resolveShutDownThreadPools() {
        return shutDownThreadPools == null ? Boolean.TRUE : shutDownThreadPools;
    }

    /**
     * @return The minimum upload part size currently configured in the builder.
     */
    public final Long getMinimumUploadPartSize() {
        return minimumUploadPartSize;
    }

    /**
     * Sets the minimum part size for upload parts. Decreasing the minimum part size will cause
     * multipart uploads to be split into a larger number of smaller parts. Setting this value too
     * low can have a negative effect on transfer speeds since it will cause extra latency and
     * network communication for each part.
     *
     * @param minimumUploadPartSize New minimum threshold for upload part size
     */
    public final void setMinimumUploadPartSize(Long minimumUploadPartSize) {
        this.minimumUploadPartSize = minimumUploadPartSize;
    }

    /**
     * Sets the minimum part size for upload parts. Decreasing the minimum part size will cause
     * multipart uploads to be split into a larger number of smaller parts. Setting this value too
     * low can have a negative effect on transfer speeds since it will cause extra latency and
     * network communication for each part.
     *
     * @param minimumUploadPartSize New minimum threshold for upload part size
     * @return This object for method chaining.
     */
    public final TransferManagerBuilder withMinimumUploadPartSize(Long minimumUploadPartSize) {
        setMinimumUploadPartSize(minimumUploadPartSize);
        return this;
    }

    /**
     * @return The multipart upload threshold currently configured in the builder.
     */
    public final Long getMultipartUploadThreshold() {
        return multipartUploadThreshold;
    }

    /**
     * Sets the size threshold, in bytes, for when to use multipart uploads. Uploads over this size
     * will automatically use a multipart upload strategy, while uploads smaller than this threshold
     * will use a single connection to upload the whole object.
     *
     * <p>Multipart uploads are easier to recover from and also potentially faster than single part
     * uploads, especially when the upload parts can be uploaded in parallel as with files. Because
     * there is additional network communication, small uploads are still recommended to use a
     * single connection for the upload.</p>
     *
     * @param multipartUploadThreshold Threshold in which multipart uploads will be performed.
     */
    public final void setMultipartUploadThreshold(Long multipartUploadThreshold) {
        this.multipartUploadThreshold = multipartUploadThreshold;
    }

    /**
     * Sets the size threshold, in bytes, for when to use multipart uploads. Uploads over this size
     * will automatically use a multipart upload strategy, while uploads smaller than this threshold
     * will use a single connection to upload the whole object.
     *
     * <p>Multipart uploads are easier to recover from and also potentially faster than single part
     * uploads, especially when the upload parts can be uploaded in parallel as with files. Because
     * there is additional network communication, small uploads are still recommended to use a
     * single connection for the upload.</p>
     *
     * @param multipartUploadThreshold Threshold in which multipart uploads will be performed.
     * @return This object for method chaining.
     */
    public final TransferManagerBuilder withMultipartUploadThreshold(
            Long multipartUploadThreshold) {
        setMultipartUploadThreshold(multipartUploadThreshold);
        return this;
    }

    /**
     * @return The multipart copy threshold currently configured in the builder.
     */
    public final Long getMultipartCopyThreshold() {
        return multipartCopyThreshold;
    }

    /**
     * Sets the size threshold, in bytes, for when to use multi-part copy. Copy requests for objects
     * over this size will automatically use a multi-part copy strategy, while copy requests for
     * objects smaller than this threshold will use a single connection to copy the whole object.
     *
     * @param multipartCopyThreshold Threshold in which multipart copies will be performed.
     */
    public final void setMultipartCopyThreshold(Long multipartCopyThreshold) {
        this.multipartCopyThreshold = multipartCopyThreshold;
    }

    /**
     * Sets the size threshold, in bytes, for when to use multi-part copy. Copy requests for objects
     * over this size will automatically use a multi-part copy strategy, while copy requests for
     * objects smaller than this threshold will use a single connection to copy the whole object.
     *
     * @param multipartCopyThreshold Threshold in which multipart copies will be performed.
     * @return This object for method chaining.
     */
    public final TransferManagerBuilder withMultipartCopyThreshold(Long multipartCopyThreshold) {
        setMultipartCopyThreshold(multipartCopyThreshold);
        return this;
    }

    /**
     * @return The multipart copy part size currently configured in the builder.
     */
    public final Long getMultipartCopyPartSize() {
        return multipartCopyPartSize;
    }

    /**
     * Sets the minimum size in bytes of each part when a multi-part copy operation is carried out.
     * Decreasing the minimum part size will cause a large number of copy part requests being
     * initiated.
     *
     * @param multipartCopyPartSize New minimum threshold for copy part size
     */
    public final void setMultipartCopyPartSize(Long multipartCopyPartSize) {
        this.multipartCopyPartSize = multipartCopyPartSize;
    }

    /**
     * Sets the minimum size in bytes of each part when a multi-part copy operation is carried out.
     * Decreasing the minimum part size will cause a large number of copy part requests being
     * initiated.
     *
     * @param multipartCopyPartSize New minimum threshold for copy part size
     * @return This object for method chaining.
     */
    public final TransferManagerBuilder withMultipartCopyPartSize(Long multipartCopyPartSize) {
        setMultipartCopyPartSize(multipartCopyPartSize);
        return this;
    }

    /**
     * Returns if the parallel downloads are disabled or not. By default, the value is set to false.
     *
     * <p>
     * TransferManager automatically detects and downloads a multipart object
     * in parallel. Setting this option to true will disable parallel downloads.
     * </p>
     * <p>
     * During parallel downloads, each part is downloaded to a temporary file, gets merged
     * into the final destination file and will be deleted. These temporary files uses disk space temporarily.
     * Disable parallel downloads if your system do not have enough space to store these files during download.
     * </p>
     * <p>
     * Disabling parallel downloads might reduce performance for large files.
     * </p>
     *
     * @return true if parallel downloads are disabled, otherwise false.
     */
    public Boolean isDisableParallelDownloads() {
        return disableParallelDownloads;
    }

    /**
     * Sets the option to disable parallel downloads. By default, the value is set to false.
     *
     * <p>
     * TransferManager automatically detects and downloads a multipart object
     * in parallel. Setting this option to true will disable parallel downloads.
     * </p>
     * <p>
     * During parallel downloads, each part is downloaded to a temporary file, gets merged
     * into the final destination file and will be deleted. These temporary files uses disk space temporarily.
     * Disable parallel downloads if your system do not have enough space to store these files during download.
     * </p>
     * <p>
     * Disabling parallel downloads might reduce performance for large files.
     * </p>
     *
     * @param disableParallelDownloads boolean value to disable parallel downloads.
     */
    public void setDisableParallelDownloads(Boolean disableParallelDownloads) {
        this.disableParallelDownloads = disableParallelDownloads;
    }

    /**
     * Sets the option to disable parallel downloads. By default, the value is set to false.
     *
     * <p>
     * TransferManager automatically detects and downloads a multipart object
     * in parallel. Setting this option to true will disable parallel downloads.
     * </p>
     * <p>
     * During parallel downloads, each part is downloaded to a temporary file, gets merged
     * into the final destination file and will be deleted. These temporary files uses disk space temporarily.
     * Disable parallel downloads if your system do not have enough space to store these files during download.
     * </p>
     * <p>
     * Disabling parallel downloads might reduce performance for large files.
     * </p>
     *
     * @param disableParallelDownloads boolean value to disable parallel downloads.
     * @return this object for method changing
     */
    public TransferManagerBuilder withDisableParallelDownloads(Boolean disableParallelDownloads) {
        setDisableParallelDownloads(disableParallelDownloads);
        return this;
    }

    /**
     * Disables parallel downloads, see {@link #setDisableParallelDownloads(Boolean)}
     * <p>
     * Disabling parallel downloads might reduce performance for large files.
     * </p>
     *
     * @return This object for method chaining
     */
    public TransferManagerBuilder disableParallelDownloads() {
        return withDisableParallelDownloads(Boolean.TRUE);
    }

    private TransferManagerConfiguration resolveConfiguration() {
        TransferManagerConfiguration configuration = new TransferManagerConfiguration();
        if (this.minimumUploadPartSize != null) {
            configuration.setMinimumUploadPartSize(minimumUploadPartSize);
        }
        if (this.multipartCopyPartSize != null) {
            configuration.setMultipartCopyPartSize(multipartCopyPartSize);
        }
        if (this.multipartCopyThreshold != null) {
            configuration.setMultipartCopyThreshold(multipartCopyThreshold);
        }
        if (this.multipartUploadThreshold != null) {
            configuration.setMultipartUploadThreshold(multipartUploadThreshold);
        }

        if (this.disableParallelDownloads != null) {
            configuration.setDisableParallelDownloads(disableParallelDownloads);
        }
        return configuration;
    }

    TransferManagerParams getParams() {
        return new TransferManagerParams().withS3Client(resolveS3Client())
                .withExecutorService(resolveExecutorService())
                .withShutDownThreadPools(resolveShutDownThreadPools())
                .withTransferManagerConfiguration(resolveConfiguration());
    }

    /**
     * Construct a TransferManager with the default ExecutorService.
     *
     * @return TransferManager with configured AmazonS3 client.
     */
    public final TransferManager build() {
        return transferManagerFactory.apply(getParams());
    }

}
