/**
 * Copyright (c) 2000-2013 Liferay, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of the Liferay Enterprise
 * Subscription License ("License"). You may not use this file except in
 * compliance with the License. You can obtain a copy of the License by
 * contacting Liferay, Inc. See the License for the specific language governing
 * permissions and limitations under the License, including but not limited to
 * distribution rights of the Software.
 *
 *
 *
 */

package com.liferay.portlet.documentlibrary.store;

import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.util.CharPool;
import com.liferay.portal.kernel.util.DateUtil;
import com.liferay.portal.kernel.util.FileUtil;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.LocaleUtil;
import com.liferay.portal.kernel.util.PropsKeys;
import com.liferay.portal.kernel.util.ReleaseInfo;
import com.liferay.portal.kernel.util.StreamUtil;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.SystemProperties;
import com.liferay.portal.kernel.util.Time;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.kernel.uuid.PortalUUIDUtil;
import com.liferay.portal.util.PropsUtil;
import com.liferay.portal.util.PropsValues;
import com.liferay.portlet.documentlibrary.DuplicateFileException;
import com.liferay.portlet.documentlibrary.NoSuchFileException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;

import org.jets3t.service.Jets3tProperties;
import org.jets3t.service.S3Service;
import org.jets3t.service.S3ServiceException;
import org.jets3t.service.ServiceException;
import org.jets3t.service.impl.rest.httpclient.RestS3Service;
import org.jets3t.service.model.S3Bucket;
import org.jets3t.service.model.S3Object;
import org.jets3t.service.model.StorageObject;
import org.jets3t.service.security.AWSCredentials;
import org.jets3t.service.utils.MultipartUtils;

/**
 * @author Brian Wing Shun Chan
 * @author Sten Martinez
 * @author Edward Han
 * @author Vilmos Papp
 * @author Mate Thurzo
 */
public class S3Store extends BaseStore {

	public S3Store() {
		try {
			_s3Service = getS3Service();
			_s3Bucket = getS3Bucket();
		}
		catch (S3ServiceException s3se) {
			_log.error(s3se.getMessage());
		}
	}

	@Override
	public void addDirectory(
		long companyId, long repositoryId, String dirName) {
	}

	@Override
	public void addFile(
			long companyId, long repositoryId, String fileName, InputStream is)
		throws SystemException {

		try {
			File file = FileUtil.createTempFile(is);

			addFile(companyId, repositoryId, fileName, file);
		}
		catch (IOException ioe) {
			throw new SystemException(ioe);
		}
		finally {
			StreamUtil.cleanUp(is);
		}
	}

	@Override
	public void addFile(
			long companyId, long repositoryId, String fileName, File file)
		throws SystemException {

		putObject(
			getKey(companyId, repositoryId, fileName, VERSION_DEFAULT), file);
	}

	@Override
	public void checkRoot(long companyId) {
	}

	@Override
	public void deleteDirectory(
			long companyId, long repositoryId, String dirName)
		throws SystemException {

		try {
			S3Object[] s3Objects = _s3Service.listObjects(
				_s3Bucket.getName(), getKey(companyId, repositoryId, dirName),
				null);

			for (S3Object s3Object : s3Objects) {
				_s3Service.deleteObject(_s3Bucket, s3Object.getKey());
			}
		}
		catch (S3ServiceException s3se) {
			throw new SystemException(s3se);
		}
	}

	@Override
	public void deleteFile(long companyId, long repositoryId, String fileName)
		throws SystemException {

		try {
			S3Object[] s3Objects = _s3Service.listObjects(
				_s3Bucket.getName(), getKey(companyId, repositoryId, fileName),
				null);

			for (S3Object s3Object : s3Objects) {
				_s3Service.deleteObject(_s3Bucket, s3Object.getKey());
			}
		}
		catch (S3ServiceException s3se) {
			throw new SystemException(s3se);
		}
	}

	@Override
	public void deleteFile(
			long companyId, long repositoryId, String fileName,
			String versionLabel)
		throws SystemException {

		try {
			_s3Service.deleteObject(
				_s3Bucket,
				getKey(companyId, repositoryId, fileName, versionLabel));
		}
		catch (S3ServiceException s3se) {
			throw new SystemException(s3se);
		}
	}

	@Override
	public File getFile(
			long companyId, long repositoryId, String fileName,
			String versionLabel)
		throws PortalException, SystemException {

		try {
			if (Validator.isNull(versionLabel)) {
				versionLabel = getHeadVersionLabel(
					companyId, repositoryId, fileName);
			}

			S3Object s3Object = _s3Service.getObject(
				_s3Bucket.getName(),
				getKey(companyId, repositoryId, fileName, versionLabel));

			File tempFile = getTempFile(s3Object, fileName);

			cleanUpTempFiles();

			return tempFile;
		}
		catch (IOException ioe) {
			throw new SystemException(ioe);
		}
		catch (ServiceException se) {
			throw new SystemException(se);
		}
	}

	@Override
	public InputStream getFileAsStream(
			long companyId, long repositoryId, String fileName,
			String versionLabel)
		throws PortalException, SystemException {

		try {
			if (Validator.isNull(versionLabel)) {
				versionLabel = getHeadVersionLabel(
					companyId, repositoryId, fileName);
			}

			S3Object s3Object = _s3Service.getObject(
				_s3Bucket.getName(),
				getKey(companyId, repositoryId, fileName, versionLabel));

			return s3Object.getDataInputStream();
		}
		catch (ServiceException se) {
			throw new SystemException(se);
		}
	}

	@Override
	public String[] getFileNames(long companyId, long repositoryId)
		throws SystemException {

		try {
			S3Object[] s3Objects = _s3Service.listObjects(
				_s3Bucket.getName(), getKey(companyId, repositoryId), null);

			return getFileNames(s3Objects);
		}
		catch (S3ServiceException s3se) {
			throw new SystemException(s3se);
		}
	}

	@Override
	public String[] getFileNames(
			long companyId, long repositoryId, String dirName)
		throws SystemException {

		try {
			S3Object[] s3Objects = _s3Service.listObjects(
				_s3Bucket.getName(), getKey(companyId, repositoryId, dirName),
				null);

			return getFileNames(s3Objects);
		}
		catch (S3ServiceException s3se) {
			throw new SystemException(s3se);
		}
	}

	@Override
	public long getFileSize(long companyId, long repositoryId, String fileName)
		throws PortalException, SystemException {

		try {
			String versionLabel = getHeadVersionLabel(
				companyId, repositoryId, fileName);

			StorageObject storageObject = _s3Service.getObjectDetails(
				_s3Bucket.getName(),
				getKey(companyId, repositoryId, fileName, versionLabel));

			return storageObject.getContentLength();
		}
		catch (ServiceException se) {
			throw new SystemException(se);
		}
	}

	@Override
	public boolean hasDirectory(
		long companyId, long repositoryId, String dirName) {

		return true;
	}

	@Override
	public boolean hasFile(
			long companyId, long repositoryId, String fileName,
			String versionLabel)
		throws SystemException {

		try {
			S3Object[] s3Objects = _s3Service.listObjects(
				_s3Bucket.getName(),
				getKey(companyId, repositoryId, fileName, versionLabel), null);

			if (s3Objects.length == 0) {
				return false;
			}
			else {
				return true;
			}
		}
		catch (S3ServiceException s3se) {
			throw new SystemException(s3se);
		}
	}

	@Override
	public void move(String srcDir, String destDir) {
	}

	@Override
	public void updateFile(
			long companyId, long repositoryId, long newRepositoryId,
			String fileName)
		throws PortalException, SystemException {

		File tempFile = null;
		InputStream is = null;
		S3Object newS3Object = null;

		if (repositoryId == newRepositoryId) {
			throw new DuplicateFileException(
				String.format(
					"{companyId=%s, fileName=%s, repositoryId=%s}", companyId,
					fileName, repositoryId));
		}

		try {
			S3Object[] s3Objects = _s3Service.listObjects(
				_s3Bucket.getName(), getKey(companyId, repositoryId, fileName),
				null);

			for (S3Object oldS3Object : s3Objects) {
				String oldKey = oldS3Object.getKey();

				oldS3Object = _s3Service.getObject(_s3Bucket.getName(), oldKey);

				tempFile = new File(
					SystemProperties.get(SystemProperties.TMP_DIR) +
						File.separator + PortalUUIDUtil.generate());

				FileUtil.write(tempFile, oldS3Object.getDataInputStream());

				is = new FileInputStream(tempFile);

				String newPrefix = getKey(companyId, newRepositoryId);

				int x = oldKey.indexOf(CharPool.SLASH);

				x = oldKey.indexOf(CharPool.SLASH, x + 1);

				String newKey = newPrefix + oldKey.substring(x);

				putObject(newKey, tempFile);

				_s3Service.deleteObject(_s3Bucket, oldKey);
			}
		}
		catch (IOException ioe) {
			throw new SystemException(ioe);
		}
		catch (ServiceException se) {
			throw new SystemException(se);
		}
		finally {
			StreamUtil.cleanUp(is);

			FileUtil.delete(tempFile);
		}
	}

	@Override
	public void updateFile(
			long companyId, long repositoryId, String fileName,
			String newFileName)
		throws PortalException, SystemException {

		if (fileName.equals(newFileName)) {
			throw new DuplicateFileException(
				String.format(
					"{companyId=%s, fileName=%s, repositoryId=%s}", companyId,
					fileName, repositoryId));
		}

		File tempFile = null;
		InputStream is = null;
		S3Object newS3Object = null;

		try {
			S3Object[] s3Objects = _s3Service.listObjects(
				_s3Bucket.getName(), getKey(companyId, repositoryId, fileName),
				null);

			for (S3Object oldS3Object : s3Objects) {
				String oldKey = oldS3Object.getKey();

				oldS3Object = _s3Service.getObject(_s3Bucket.getName(), oldKey);

				tempFile = new File(
					SystemProperties.get(SystemProperties.TMP_DIR) +
						File.separator + PortalUUIDUtil.generate());

				FileUtil.write(tempFile, oldS3Object.getDataInputStream());

				oldS3Object.closeDataInputStream();

				is = new FileInputStream(tempFile);

				String newPrefix = getKey(companyId, repositoryId, newFileName);

				int x = oldKey.indexOf(StringPool.SLASH);

				x = oldKey.indexOf(CharPool.SLASH, x + 1);
				x = oldKey.indexOf(CharPool.SLASH, x + 1);

				String newKey = newPrefix + oldKey.substring(x);

				putObject(newKey, tempFile);

				_s3Service.deleteObject(_s3Bucket, oldKey);
			}
		}
		catch (IOException ioe) {
			throw new SystemException(ioe);
		}
		catch (ServiceException se) {
			throw new SystemException(se);
		}
		finally {
			StreamUtil.cleanUp(is);

			FileUtil.delete(tempFile);
		}
	}

	@Override
	public void updateFile(
			long companyId, long repositoryId, String fileName,
			String versionLabel, InputStream is)
		throws SystemException {

		File file = null;

		try {
			file = FileUtil.createTempFile(is);

			updateFile(companyId, repositoryId, fileName, versionLabel, file);
		}
		catch (Exception e) {
			throw new SystemException(e);
		}
		finally {
			StreamUtil.cleanUp(is);

			FileUtil.delete(file);
		}
	}

	@Override
	public void updateFile(
			long companyId, long repositoryId, String fileName,
			String versionLabel, File file)
		throws PortalException, SystemException {

		putObject(
			getKey(companyId, repositoryId, fileName, versionLabel), file);
	}

	protected void cleanUpTempFiles() {
		_calledGetFileCount++;

		if (_calledGetFileCount <
				PropsValues.DL_STORE_S3_TEMP_DIR_CLEAN_UP_FREQUENCY) {

			return;
		}

		synchronized (this) {
			if (_calledGetFileCount == 0) {
				return;
			}

			_calledGetFileCount = 0;

			String tempDirName =
				SystemProperties.get(SystemProperties.TMP_DIR) + _TEMP_DIR_NAME;

			File tempDir = new File(tempDirName);

			long lastModified = System.currentTimeMillis();

			lastModified -=
				(PropsValues.DL_STORE_S3_TEMP_DIR_CLEAN_UP_EXPUNGE * Time.DAY);

			cleanUpTempFiles(tempDir, lastModified);
		}
	}

	protected void cleanUpTempFiles(File file, long lastModified) {
		if (!file.isDirectory()) {
			return;
		}

		String[] fileNames = FileUtil.listDirs(file);

		if (fileNames.length == 0) {
			if (file.lastModified() < lastModified) {
				FileUtil.deltree(file);

				return;
			}
		}
		else {
			for (String fileName : fileNames) {
				cleanUpTempFiles(new File(file, fileName), lastModified);
			}

			String[] subfileNames = file.list();

			if (subfileNames.length == 0) {
				FileUtil.deltree(file);

				return;
			}
		}
	}

	protected AWSCredentials getAWSCredentials() throws S3ServiceException {
		if (Validator.isNull(_ACCESS_KEY) || Validator.isNull(_SECRET_KEY)) {
			throw new S3ServiceException(
				"S3 access and secret keys are not set");
		}
		else {
			return new AWSCredentials(_ACCESS_KEY, _SECRET_KEY);
		}
	}

	protected String getFileName(String key) {

		// Convert /${companyId}/${repositoryId}/${dirName}/${fileName}
		// /${versionLabel} to /${dirName}/${fileName}

		int x = key.indexOf(CharPool.SLASH);

		x = key.indexOf(CharPool.SLASH, x + 1);

		int y = key.lastIndexOf(CharPool.SLASH);

		return key.substring(x, y);
	}

	protected String[] getFileNames(S3Object[] s3Objects) {
		List<String> fileNames = new ArrayList<String>();

		for (S3Object s3Object : s3Objects) {
			String fileName = getFileName(s3Object.getKey());

			fileNames.add(fileName);
		}

		return fileNames.toArray(new String[fileNames.size()]);
	}

	protected String getHeadVersionLabel(
			long companyId, long repositoryId, String fileName)
		throws PortalException, S3ServiceException {

		S3Object[] s3Objects = _s3Service.listObjects(
			_s3Bucket.getName(), getKey(companyId, repositoryId, fileName),
			null);

		String[] keys = new String[s3Objects.length];

		for (int i = 0; i < s3Objects.length; i++) {
			S3Object s3Object = s3Objects[i];

			keys[i] = s3Object.getKey();
		}

		if (keys.length > 0) {
			Arrays.sort(keys);

			String headKey = keys[keys.length - 1];

			int x = headKey.lastIndexOf(CharPool.SLASH);

			return headKey.substring(x + 1);
		}
		else {
			throw new NoSuchFileException(fileName);
		}
	}

	protected Jets3tProperties getJets3tProperties() {
		Jets3tProperties jets3tProperties = new Jets3tProperties();

		Properties properties = new Properties();

		for (Entry<Object, Object> entry : _jets3tProperties.entrySet()) {
			String key = (String)entry.getKey();
			String value = (String)entry.getValue();

			properties.put(StringUtil.strip(key, _BRACKETS), value);
		}

		jets3tProperties.loadAndReplaceProperties(properties, "liferay");

		if (_log.isInfoEnabled()) {
			_log.info("Jets3t properties: " + jets3tProperties.getProperties());
		}

		return jets3tProperties;
	}

	protected String getKey(long companyId, long repositoryId) {
		StringBundler sb = new StringBundler(4);

		sb.append(companyId);
		sb.append(StringPool.SLASH);
		sb.append(repositoryId);

		return sb.toString();
	}

	protected String getKey(
		long companyId, long repositoryId, String fileName) {

		StringBundler sb = new StringBundler(4);

		sb.append(companyId);
		sb.append(StringPool.SLASH);
		sb.append(repositoryId);
		sb.append(getNormalizedFileName(fileName));

		return sb.toString();
	}

	protected String getKey(
		long companyId, long repositoryId, String fileName,
		String versionLabel) {

		StringBundler sb = new StringBundler(6);

		sb.append(companyId);
		sb.append(StringPool.SLASH);
		sb.append(repositoryId);
		sb.append(getNormalizedFileName(fileName));
		sb.append(StringPool.SLASH);
		sb.append(versionLabel);

		return sb.toString();
	}

	protected String getNormalizedFileName(String fileName) {
		String normalizedFileName = fileName;

		if (!fileName.startsWith(StringPool.SLASH)) {
			normalizedFileName = StringPool.SLASH + normalizedFileName;
		}

		if (fileName.endsWith(StringPool.SLASH)) {
			normalizedFileName = normalizedFileName.substring(
				0, normalizedFileName.length() - 1);
		}

		return normalizedFileName;
	}

	protected S3Bucket getS3Bucket() throws S3ServiceException {
		if (Validator.isNull(_BUCKET_NAME)) {
			throw new S3ServiceException("S3 bucket name is not set");
		}
		else {
			return getS3Service().getBucket(_BUCKET_NAME);
		}
	}

	protected S3Service getS3Service() throws S3ServiceException {
		AWSCredentials credentials = getAWSCredentials();

		Jets3tProperties jets3tProperties = getJets3tProperties();

		return new RestS3Service(
			credentials, ReleaseInfo.getServerInfo(), null, jets3tProperties);
	}

	protected File getTempFile(S3Object s3Object, String fileName)
		throws IOException, ServiceException {

		StringBundler sb = new StringBundler(5);

		sb.append(SystemProperties.get(SystemProperties.TMP_DIR));
		sb.append(_TEMP_DIR_NAME);
		sb.append(
			DateUtil.getCurrentDate(
				_TEMP_DIR_PATTERN, LocaleUtil.getDefault()));
		sb.append(getNormalizedFileName(fileName));

		Date lastModifiedDate = s3Object.getLastModifiedDate();

		sb.append(lastModifiedDate.getTime());

		String tempFileName = sb.toString();

		File tempFile = new File(tempFileName);

		InputStream inputStream = s3Object.getDataInputStream();

		if (tempFile.exists() &&
			(tempFile.lastModified() >= lastModifiedDate.getTime())) {

			StreamUtil.cleanUp(inputStream);

			return tempFile;
		}

		if (inputStream == null) {
			throw new IOException("S3 object input stream is null");
		}

		OutputStream outputStream = null;

		try {
			File parentFile = tempFile.getParentFile();

			FileUtil.mkdirs(parentFile);

			outputStream = new FileOutputStream(tempFile);

			StreamUtil.transfer(inputStream, outputStream);
		}
		finally {
			StreamUtil.cleanUp(inputStream, outputStream);
		}

		return tempFile;
	}

	protected void putObject(String key, File file) throws SystemException {
		try {
			MultipartUtils multipartUtils = new MultipartUtils(_PART_SIZE);

			List<StorageObject> s3Objects = new ArrayList<StorageObject>();

			S3Object s3Object = new S3Object(file);

			s3Object.setBucketName(_BUCKET_NAME);
			s3Object.setKey(key);

			s3Objects.add(s3Object);

			multipartUtils.uploadObjects(
				_BUCKET_NAME, _s3Service, s3Objects, null);
		}
		catch (Exception e) {
			throw new SystemException(e);
		}
	}

	private static final String _ACCESS_KEY = PropsUtil.get(
		PropsKeys.DL_STORE_S3_ACCESS_KEY);

	private static char[] _BRACKETS =
		{CharPool.CLOSE_BRACKET, CharPool.OPEN_BRACKET};

	private static final String _BUCKET_NAME = PropsUtil.get(
		PropsKeys.DL_STORE_S3_BUCKET_NAME);

	private static final long _PART_SIZE = GetterUtil.getLong(
		PropsUtil.get(PropsKeys.DL_STORE_S3_PART_SIZE));

	private static final String _SECRET_KEY = PropsUtil.get(
		PropsKeys.DL_STORE_S3_SECRET_KEY);

	private static final String _TEMP_DIR_NAME = "/liferay/s3";

	private static final String _TEMP_DIR_PATTERN = "/yyyy/MM/dd/HH/";

	private static Log _log = LogFactoryUtil.getLog(S3Store.class);

	private static final Properties _jets3tProperties = PropsUtil.getProperties(
		PropsKeys.DL_STORE_S3_JETS3T, true);

	private int _calledGetFileCount;
	private S3Bucket _s3Bucket;
	private S3Service _s3Service;

}