package com.nifty.cloud.ncs.transfer;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.BufferedInputStream;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import org.apache.commons.codec.binary.Hex;

import com.nifty.cloud.ncs.NiftyCloudClientException;
import com.nifty.cloud.ncs.NiftyCloudFileInputStream;
import com.nifty.cloud.ncs.NiftyCloudInputSubstream;
import com.nifty.cloud.ncs.service.UploadPartRequest;

/**
 * パートアップロードのリクエストを生成するクラスです。
 */
public class UploadPartRequestFactory
{
	private final String bucketName;
	private final String objectName;
	private final String uploadId;
	private final long partSize;
	private File file;
	private InputStream inputStream;
	private int partNumber = 1;
	private long offset = 0;
	private long remainingBytes;
	private boolean isParallel = false;
	private long contentLength;
	private String[] sha256HashArray;


	/**
	 * 入力ストリームを指定してUploadPartRequestFactoryを構築します。
	 *
	 * @param bucketName
	 *  バケット名
	 * @param objectName
	 *  オブジェクト名
	 * @param inputStream
	 *  オブジェクトのボディとなる入力ストリーム
	 * @param contentLength
	 *  データサイズ
	 * @param uploadId
	 *  マルチパートアップロードID
	 * @param partSize
	 *  パート毎のサイズ
	 */
	public UploadPartRequestFactory(String bucketName, String objectName, InputStream inputStream, long contentLength, String uploadId, long partSize)
	{
		this(bucketName, objectName, uploadId, partSize);
		this.inputStream = inputStream;
		this.remainingBytes = contentLength;
		this.contentLength = contentLength;
	}

	/**
	 * ファイルを指定してUploadPartRequestFactoryを構築します。
	 *
	 * @param bucketName
	 *  バケット名
	 * @param objectName
	 *  オブジェクト名
	 * @param file
	 *  アップロードするファイル
	 * @param uploadId
	 *  マルチパートアップロードID
	 * @param partSize
	 *  パート毎のサイズ
	 */
	public UploadPartRequestFactory(String bucketName, String objectName, File file, String uploadId, long partSize)
			throws NiftyCloudClientException
	{
		this(bucketName, objectName, uploadId, partSize);
		if (file == null) {
			throw (new NiftyCloudClientException("The argument of file must be specified."));
		}
		this.file = file;
		this.remainingBytes = file.length();
		this.contentLength = file.length();
	}

	/**
	 * UploadPartRequestFactoryを構築します。
	 * このコンストラクタは内部でのみ使用されます。
	 *
	 * @param object
	 * @param uploadId
	 * @param partSize
	 */
	private UploadPartRequestFactory(String bucketName, String objectName, String uploadId, long partSize)
	{
		this.uploadId = uploadId.trim();
		this.partSize = partSize;
		this.bucketName = bucketName;
		this.objectName = objectName;
	}

	/**
	 * 次のリクエストがあるかどうかの判定をします。
	 *
	 * @return 次のリクエストがあるかどうかのフラグ
	 */
	public synchronized boolean hasMoreRequests()
	{
		return (remainingBytes > 0);
	}

	/**
	 * 次のパートのリクエストを取得します。
	 *
	 * @return 次のパートのリクエスト
	 */
	public synchronized UploadPartRequest getNextUploadPartRequest()
			throws NiftyCloudClientException
	{
		long partSize = Math.min(this.partSize, remainingBytes);
		boolean isLastPart = (remainingBytes - partSize <= 0);
		int currentHashArrayIndex = partNumber - 1;
		UploadPartRequest request = null;
		request = new UploadPartRequest()
		.withBucketName(bucketName)
		.withKey(objectName)
		.withUploadId(uploadId)
		.withPartNumber(partNumber++)
		.withPartSize((int) partSize);

		String contentSHA256 = "";
		if (file != null) {
			if (isParallel) {
				request.withFile(file)
				.withFileOffset(offset);
			}
			else {
				try {
					// 毎回offsetまでのスキップ処理が入ってしまうため、直列処理の場合は
					// 同じInputStreamを使いまわすようにする
					inputStream = new NiftyCloudFileInputStream(file);

					file = null;
				} catch (FileNotFoundException e) {
					throw ( new NiftyCloudClientException(
							"The specified file doesn't exist", e) );
				}
			}
		}

		if (inputStream != null) {
			if (sha256HashArray == null) {
				try {
					byte[] content = new byte[(int)contentLength];
					int bytesRead = inputStream.read(content);
					if (bytesRead != contentLength) {
						throw (new NiftyCloudClientException("Can't duplicate stream"));
					}
					inputStream.close();

					InputStream streamForHash = new ByteArrayInputStream(content);
					computeSHA256HashArray(streamForHash);
					streamForHash.close();

					inputStream = new ByteArrayInputStream(content);
				} catch (IOException e) {
					throw (new NiftyCloudClientException("Can't duplicate stream: " + e.getMessage(), e));
				} catch (NoSuchAlgorithmException e) {
					throw (new NiftyCloudClientException("Can't calculate SHA256 hash: " + e.getMessage(), e));
				}
			}

			request.setInputStream(new NiftyCloudInputSubstream(inputStream, 0, partSize, isLastPart), sha256HashArray[currentHashArrayIndex]);
		}

		offset += partSize;
		remainingBytes -= partSize;

		request.setLastPart(isLastPart);

		return (request);
	}

	/**
	 * パートが並列でアップロードされるかどうかを設定します。<br>
	 * 並列でアップロードされる場合は必ず<code>true</code>に設定してください。<br>
	 * Fileを使用する場合のみ並列アップロードが可能となります。
	 *
	 * @param isParallel
	 *  パートが並列でアップロードされるかどうかのフラグ
	 * @throws NiftyCloudClientException
	 */
	public void setParallel(boolean isParallel) throws NiftyCloudClientException
	{
		if (file == null && isParallel) {
			throw (new NiftyCloudClientException("Unable to upload in parallel when using InputStream"));
		}

		this.isParallel = isParallel;
	}

	private void computeSHA256HashArray(InputStream is) throws NoSuchAlgorithmException, IOException
	{
		BufferedInputStream bis = new BufferedInputStream(is);

		this.sha256HashArray = new String[(int)((contentLength + partSize - 1 ) / partSize)];

		try {
			int arrayIndex = 0;

			byte[] buffer = new byte[(int)partSize];
			int totalReadBytes = 0;
			while (true) {
				if (totalReadBytes >= contentLength) {
					break;
				}

				MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
				long lengthToRead = Math.min(partSize, (contentLength - totalReadBytes));
				int readBytes = bis.read(buffer, 0, (int)lengthToRead);
				if (readBytes != lengthToRead) {
					throw (new NiftyCloudClientException("Can't calculate SHA256 hash"));
				}
				totalReadBytes += readBytes;
				messageDigest.update(buffer, 0, readBytes);

				sha256HashArray[arrayIndex++] = Hex.encodeHexString(messageDigest.digest());
			}
		} finally {
			try {
				bis.close();
			} catch (Exception e) {
				throw (new NiftyCloudClientException("Can't close input stream of hash candidate"));
			}
		}
	}
}
