/*
 * LinShare is an open source filesharing software, part of the LinPKI software
 * suite, developed by Linagora.
 * 
 * Copyright (C) 2015 LINAGORA
 * 
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version, provided you comply with the Additional Terms applicable for
 * LinShare software by Linagora pursuant to Section 7 of the GNU Affero General
 * Public License, subsections (b), (c), and (e), pursuant to which you must
 * notably (i) retain the display of the “LinShare™” trademark/logo at the top
 * of the interface window, the display of the “You are using the Open Source
 * and free version of LinShare™, powered by Linagora © 2009–2015. Contribute to
 * Linshare R&D by subscribing to an Enterprise offer!” infobox and in the
 * e-mails sent with the Program, (ii) retain all hypertext links between
 * LinShare and linshare.org, between linagora.com and Linagora, and (iii)
 * refrain from infringing Linagora intellectual property rights over its
 * trademarks and commercial brands. Other Additional Terms apply, see
 * <http://www.linagora.com/licenses/> for more details.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Affero General Public License and
 * its applicable Additional Terms for LinShare along with this program. If not,
 * see <http://www.gnu.org/licenses/> for the GNU Affero General Public License
 * version 3 and <http://www.linagora.com/licenses/> for the Additional Terms
 * applicable to LinShare software.
 */
package org.linagora.linshare.cmis.facade.impl;

import java.math.BigInteger;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.chemistry.opencmis.commons.data.ContentStream;
import org.apache.chemistry.opencmis.commons.data.ExtensionsData;
import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
import org.apache.chemistry.opencmis.commons.data.ObjectData;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderContainer;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderData;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderList;
import org.apache.chemistry.opencmis.commons.data.ObjectParentData;
import org.apache.chemistry.opencmis.commons.data.Properties;
import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionList;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.chemistry.opencmis.commons.enums.CapabilityChanges;
import org.apache.chemistry.opencmis.commons.enums.CmisVersion;
import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
import org.apache.chemistry.opencmis.commons.enums.VersioningState;
import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisPermissionDeniedException;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.FailedToDeleteDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectParentDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryCapabilitiesImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryInfoImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.TypeDefinitionListImpl;
import org.apache.chemistry.opencmis.commons.impl.server.AbstractCmisService;
import org.apache.chemistry.opencmis.commons.impl.server.ObjectInfoImpl;
import org.apache.chemistry.opencmis.commons.server.CmisService;
import org.apache.chemistry.opencmis.commons.server.ObjectInfo;
import org.apache.chemistry.opencmis.commons.spi.Holder;
import org.apache.chemistry.opencmis.server.support.TypeDefinitionFactory;
import org.linagora.linshare.cmis.constants.CmisConstants;
import org.linagora.linshare.cmis.constants.CmisDirectory;
import org.linagora.linshare.cmis.constants.CmisStrings;
import org.linagora.linshare.cmis.facade.MainCmisFacade;
import org.linagora.linshare.cmis.utils.CmisHelpers;
import org.linagora.linshare.core.domain.constants.SupportedLanguage;
import org.linagora.linshare.core.domain.entities.Account;
import org.linagora.linshare.core.domain.entities.Functionality;
import org.linagora.linshare.core.service.AccountService;
import org.linagora.linshare.core.service.FunctionalityReadOnlyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

public class MainCmisFacadeImpl extends AbstractCmisService implements MainCmisFacade {

	private final CmisVersion cmisVersion = CmisVersion.CMIS_1_0;
	private final AccountService accountService;
	private TypeDefinitionFactory typeDefinitionFactory = TypeDefinitionFactory
			.newInstance();
	private TypeDefinition folderTypeDefinition = typeDefinitionFactory
			.createBaseFolderTypeDefinition(cmisVersion);
	private final Logger log = LoggerFactory
			.getLogger(MainCmisFacadeImpl.class);
	private TypeDefinition documentTypeDefinition = typeDefinitionFactory
			.createBaseDocumentTypeDefinition(cmisVersion);

	private final CmisService documentEntryCmisService;
	private final CmisService threadEntryCmisService;
	private final CmisHelpers helpers;
	private final CmisStrings cmisStrings;
	private final FunctionalityReadOnlyService functionalityReadOnlyService;

	public MainCmisFacadeImpl(AccountService accountService,
			CmisService documentEntryCmisService,
			CmisService threadEntryCmisService, CmisStrings cmisStrings,
			FunctionalityReadOnlyService functionalityReadOnlyService,
			CmisHelpers cmisHelpers) {
		this.accountService = accountService;
		this.documentEntryCmisService = documentEntryCmisService;
		this.threadEntryCmisService = threadEntryCmisService;
		this.cmisStrings = cmisStrings;
		this.functionalityReadOnlyService = functionalityReadOnlyService;
		this.helpers = cmisHelpers;
	}

	@Override
	public ContentStream getContentStream(String repositoryId, String objectId,
			String streamId, BigInteger offset, BigInteger length,
			ExtensionsData extension) {
		if (objectId.startsWith(CmisConstants.tagDocumentEntry))
			return documentEntryCmisService.getContentStream(repositoryId,
					objectId, streamId, offset, length, extension);
		if (objectId.startsWith(CmisConstants.tagThreadEntry))
			return threadEntryCmisService.getContentStream(repositoryId,
					objectId, streamId, offset, length, extension);
		throw new CmisObjectNotFoundException();

	}

	private Account authenticate() {
		Authentication auth = SecurityContextHolder.getContext()
				.getAuthentication();
		if (auth == null) {
			log.debug("Not authenticated access");
			throw new CmisPermissionDeniedException();
		}
		log.debug("Authenticated Principal :" + auth.getName());
		Account currentReq = (Account) RequestContextHolder
				.getRequestAttributes().getAttribute(
						"org.linagora.linshare.cmis.currentUser",
						RequestAttributes.SCOPE_REQUEST);
		if (currentReq != null)
			return currentReq;
		Account account = accountService.findByLsUuid(auth.getName());
		Functionality cmisFunc = functionalityReadOnlyService
				.getCmisFunctionality(account.getDomain());
		if (!cmisFunc.getActivationPolicy().getStatus())
			throw new CmisPermissionDeniedException(
					"Not Authorized to use CMIS !");
		return account;
	}

	@Override
	public List<RepositoryInfo> getRepositoryInfos(ExtensionsData extension) {

		RepositoryInfoImpl r = new RepositoryInfoImpl();
		DateFormat dateFormat = new SimpleDateFormat(
				"MM-dd-yyyy_HH'h'mm'm'ss's'");

		Account account = authenticate();
		r.setId(account.getLsUuid());
		r.setName(account.getFullName() + "_" + dateFormat.format(new Date()));
		r.setRootFolder(CmisConstants.tagRoot);
		r.setCmisVersion(cmisVersion);
		RepositoryCapabilitiesImpl capabilities = new RepositoryCapabilitiesImpl();
		capabilities.setCapabilityChanges(CapabilityChanges.NONE);
		capabilities.setSupportsGetDescendants(false);
		capabilities.setSupportsGetFolderTree(false);
		r.setCapabilities(capabilities);
		List<RepositoryInfo> res = new LinkedList<RepositoryInfo>();
		res.add(r);
		return res;
	}

	@Override
	public TypeDefinitionList getTypeChildren(String repositoryId,
			String typeId, Boolean includePropertyDefinitions,
			BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
		TypeDefinitionList res = new TypeDefinitionListImpl(
				new ArrayList<TypeDefinition>());
		return res;
	}

	@Override
	public List<ObjectInFolderContainer> getDescendants(String repositoryId,
			String folderId, BigInteger depth, String filter,
			Boolean includeAllowableActions,
			IncludeRelationships includeRelationships, String renditionFilter,
			Boolean includePathSegment, ExtensionsData extension) {
		return super.getDescendants(repositoryId, folderId, depth, filter,
				includeAllowableActions, includeRelationships, renditionFilter,
				includePathSegment, extension);
	}

	@Override
	public TypeDefinition getTypeDefinition(String repositoryId, String typeId,
			ExtensionsData extension) {
		authenticate();
		TypeDefinition res = null;
		if (typeId.equals(BaseTypeId.CMIS_FOLDER.value())) {
			res = folderTypeDefinition;
		} if (typeId.equals(BaseTypeId.CMIS_DOCUMENT.value())) {
			res = documentTypeDefinition;
		}
		return res;
	}

	@Override
	public ObjectInFolderList getChildren(String repositoryId, String folderId,
			String filter, String orderBy, Boolean includeAllowableActions,
			IncludeRelationships includeRelationships, String renditionFilter,
			Boolean includePathSegment, BigInteger maxItems,
			BigInteger skipCount, ExtensionsData extension) {
		Account account = authenticate();
		ObjectInFolderListImpl res = new ObjectInFolderListImpl();
		if (folderId.equals(CmisConstants.tagRoot)) {
			List<ObjectInFolderData> objDlist = new LinkedList<ObjectInFolderData>();
			ObjectInFolderDataImpl objectInFolderDataImpl = null;

			boolean threadFunc = functionalityReadOnlyService
					.getCmisThreadsFunctionality(account.getDomain())
					.getActivationPolicy().getStatus();

			boolean docFunc = functionalityReadOnlyService
					.getCmisDocumentsFunctionality(account.getDomain())
					.getActivationPolicy().getStatus();

			if (threadFunc) {
				objectInFolderDataImpl = new ObjectInFolderDataImpl();
				objectInFolderDataImpl.setObject(getObject(repositoryId,
						CmisConstants.tagThread, filter,
						includeAllowableActions, includeRelationships,
						renditionFilter, false, false, extension));
				objDlist.add(objectInFolderDataImpl);
			}
			if (docFunc) {
				objectInFolderDataImpl = new ObjectInFolderDataImpl();
				objectInFolderDataImpl.setObject(getObject(repositoryId,
						CmisConstants.tagDocument, filter,
						includeAllowableActions, includeRelationships,
						renditionFilter, false, false, extension));
				objDlist.add(objectInFolderDataImpl);
			}
			res.setObjects(objDlist);
		} else if (folderId.equals(CmisConstants.tagDocument)) {
			res = (ObjectInFolderListImpl) documentEntryCmisService
					.getChildren(repositoryId, folderId, filter, orderBy,
							includeAllowableActions, includeRelationships,
							renditionFilter, includePathSegment, maxItems,
							skipCount, extension);
		} else if (folderId.startsWith(CmisConstants.tagThreadChildren)
				|| folderId.equals(CmisConstants.tagThread)) {
			res = (ObjectInFolderListImpl) threadEntryCmisService.getChildren(
					repositoryId, folderId, filter, orderBy,
					includeAllowableActions, includeRelationships,
					renditionFilter, includePathSegment, maxItems, skipCount,
					extension);
		} else
			throw new CmisNotSupportedException();
		return res;
	}

	@Override
	public List<ObjectParentData> getObjectParents(String repositoryId,
			String objectId, String filter, Boolean includeAllowableActions,
			IncludeRelationships includeRelationships, String renditionFilter,
			Boolean includeRelativePathSegment, ExtensionsData extension) {
		authenticate();
		String parentId = null;
		List<ObjectParentData> list = new LinkedList<ObjectParentData>();
		if (objectId.equals(CmisConstants.tagRoot) || objectId.equals(CmisConstants.tagDocument)
				|| objectId.equals(CmisConstants.tagThread)) {
			parentId = CmisConstants.tagRoot;
		} if (objectId.startsWith(CmisConstants.tagDocumentEntry)) {
			parentId = CmisConstants.tagDocument;
		}
		if (objectId.startsWith(CmisConstants.tagThreadChildren)) {
			parentId = CmisConstants.tagThread;
		}
		if (objectId.startsWith(CmisConstants.tagThreadEntry)) {
			list = threadEntryCmisService.getObjectParents(repositoryId,
					objectId, filter, includeAllowableActions,
					includeRelationships, renditionFilter,
					includeRelativePathSegment, extension);
		} else {
		ObjectParentDataImpl opd = new ObjectParentDataImpl(getObject(
				repositoryId, parentId, filter, includeAllowableActions,
				includeRelationships, renditionFilter, false, false, extension));
		opd.setRelativePathSegment(getObjectInfo(repositoryId, objectId)
				.getObject().getProperties().getPropertyList().get(4)
				.getFirstValue().toString());
		list.add(opd);
		}
		return list;
	}

	@Override
	public ObjectData getObject(String repositoryId, String objectId,
			String filter, Boolean includeAllowableActions,
			IncludeRelationships includeRelationships, String renditionFilter,
			Boolean includePolicyIds, Boolean includeAcl,
			ExtensionsData extension) {
		authenticate();
		ObjectDataImpl res = new ObjectDataImpl();
		Map<String, ObjectData> memoizer = helpers.getMemoizer();
		if (memoizer.get(objectId) != null) {
			res = (ObjectDataImpl) memoizer.get(objectId);
		}
		if (objectId.equals(CmisConstants.tagRoot)) {
			res.setProperties(helpers.setEntryProperty(objectId, "/", ""));
			res.setAllowableActions(helpers.setEntryAllowableActions(null,
					objectId));
			memoizer.put(objectId, res);
		} else if (objectId.equals(CmisConstants.tagDocument)
				|| objectId.startsWith(CmisConstants.tagDocumentEntry)) {
			res = (ObjectDataImpl) documentEntryCmisService.getObject(
					repositoryId, objectId, filter, includeAllowableActions,
					includeRelationships, renditionFilter, includePolicyIds,
					includeAcl, extension);
			memoizer.put(objectId, res);
		} else if (objectId.equals(CmisConstants.tagThread)
				|| objectId.startsWith(CmisConstants.tagThreadChildren)
				|| objectId.startsWith(CmisConstants.tagThreadEntry)) {
			res = (ObjectDataImpl) threadEntryCmisService.getObject(
					repositoryId, objectId, filter, includeAllowableActions,
					includeRelationships, renditionFilter, includePolicyIds,
					includeAcl, extension);
			memoizer.put(objectId, res);
		} else
			throw new CmisObjectNotFoundException();
		return res;
	}

	@Override
	public ObjectData getObjectByPath(String repositoryId, String path,
			String filter, Boolean includeAllowableActions,
			IncludeRelationships includeRelationships, String renditionFilter,
			Boolean includePolicyIds, Boolean includeAcl,
			ExtensionsData extension) {
		Account actor = authenticate();
		ObjectData res = null;
		if (!path.startsWith("/")) {
			throw new CmisNotSupportedException();
		}
		String[] splitPath = path.substring(1).split("/");
		if (splitPath.length == 1) {
			if (splitPath[0].equals("")) {
				res = getObject(repositoryId, CmisConstants.tagRoot, filter,
						includeAllowableActions, includeRelationships,
						renditionFilter, includePolicyIds, includeAcl,
						extension);
			} else if (splitPath[0].equals(cmisStrings.getName(CmisDirectory.MY_FILES,
					SupportedLanguage.fromTapestryLocale(actor.getCmisLocale())))) {
				res = getObject(repositoryId, CmisConstants.tagDocument, filter,
						includeAllowableActions, includeRelationships,
						renditionFilter, includePolicyIds, includeAcl,
						extension);
			} else
				throw new CmisObjectNotFoundException();
		} else if (splitPath.length == 2) {
			if (splitPath[0].equals(cmisStrings.getName(CmisDirectory.MY_FILES,
					SupportedLanguage.fromTapestryLocale(actor.getCmisLocale())))) {
				res = documentEntryCmisService.getObjectByPath(repositoryId,
						path, filter, includeAllowableActions,
						includeRelationships, renditionFilter,
						includePolicyIds, includeAcl, extension);
			} else if (splitPath[0].equals(cmisStrings
					.getName(CmisDirectory.MY_THREADS, SupportedLanguage.fromTapestryLocale(actor.getCmisLocale())))) {
				res = threadEntryCmisService.getObjectByPath(repositoryId,
						path, filter, includeAllowableActions,
						includeRelationships, renditionFilter,
						includePolicyIds, includeAcl, extension);
			} else throw new CmisObjectNotFoundException();
		} else
			throw new CmisObjectNotFoundException();
		return res;
	}

	public ObjectInfo getObjectInfo(String repositoryId, String objectId) {
		ObjectInfoImpl res;
		if (objectId.equals(CmisConstants.tagRoot)) {
			res = new ObjectInfoImpl(objectId, BaseTypeId.CMIS_FOLDER);
			res.setObject(getObject(repositoryId, objectId, null, null, null,
					null, null, null, null));
			return res;
		} else if (objectId.equals(CmisConstants.tagDocument)
				|| objectId.startsWith(CmisConstants.tagDocumentEntry)) {
			res = (ObjectInfoImpl) documentEntryCmisService.getObjectInfo(repositoryId,
					objectId);
		} else if (objectId.equals(CmisConstants.tagThread) || objectId.startsWith(CmisConstants.tagThreadChildren)
				|| objectId.startsWith(CmisConstants.tagThreadEntry)) {
			return threadEntryCmisService.getObjectInfo(repositoryId, objectId);
		} else
			throw new CmisObjectNotFoundException();
		return res;
	}

	public RepositoryInfo getRepositoryInfo(String repositoryId,
			ExtensionsData extension) {
		RepositoryInfo repo = null;
		RepositoryInfo defau = null;
		List<RepositoryInfo> list = getRepositoryInfos(extension);
		for (RepositoryInfo r : list) {
			defau = r;
			if (r.getId().equals(repositoryId))
				repo = r;
		}
		if (repo == null) {
			if (defau == null) {
				throw new CmisObjectNotFoundException();
			}
			return defau;
		}
		return repo;
	}

	@Override
	public void deleteObjectOrCancelCheckOut(String repositoryId,
			String objectId, Boolean allVersions, ExtensionsData extension) {
		authenticate();
		if (objectId.startsWith(CmisConstants.tagDocumentEntry)) {
			documentEntryCmisService.deleteObjectOrCancelCheckOut(repositoryId,
					objectId, allVersions, extension);
		} if (objectId.startsWith(CmisConstants.tagThreadEntry)) {
			threadEntryCmisService.deleteObjectOrCancelCheckOut(repositoryId,
					objectId, allVersions, extension);
		}
	}

	@Override
	public String create(String repositoryId, Properties properties,
			String folderId, ContentStream contentStream,
			VersioningState versioningState, List<String> policies,
			ExtensionsData extension) {
		authenticate();
		String res;
		if (contentStream == null) {
			log.error("Tried to sync a folder !");
			res = CmisConstants.tagRoot;
		} else if (folderId.equals(CmisConstants.tagDocument)) {
			res = documentEntryCmisService.create(repositoryId, properties,
					folderId, contentStream, versioningState, policies,
					extension);
		} else if (folderId.startsWith(CmisConstants.tagThreadChildren)) {
			res = threadEntryCmisService.create(repositoryId, properties,
					folderId, contentStream, versioningState, policies,
					extension);
		} else
			throw new CmisNotSupportedException();
		return res;
	}

	@Override
	public void setContentStream(String repositoryId, Holder<String> objectId,
			Boolean overwriteFlag, Holder<String> changeToken,
			ContentStream contentStream, ExtensionsData extension) {
		authenticate();
		if (objectId.getValue().startsWith(CmisConstants.tagDocumentEntry)) {
			documentEntryCmisService.setContentStream(repositoryId, objectId,
					overwriteFlag, changeToken, contentStream, extension);
		}
		else
			throw new CmisPermissionDeniedException();
	}

	@Override
	public void updateProperties(String repositoryId, Holder<String> objectId,
			Holder<String> changeToken, Properties properties,
			ExtensionsData extension) {
		authenticate();
		if (objectId.getValue().startsWith(CmisConstants.tagDocumentEntry)) {
			documentEntryCmisService.updateProperties(repositoryId, objectId,
					changeToken, properties, extension);
		} else if (objectId.getValue().startsWith(CmisConstants.tagThreadEntry)) {
			threadEntryCmisService.updateProperties(repositoryId, objectId,
					changeToken, properties, extension);
		} else
			throw new CmisPermissionDeniedException();
	}

	@Override
	public FailedToDeleteData deleteTree(String repositoryId, String folderId,
			Boolean allVersions, UnfileObject unfileObjects,
			Boolean continueOnFailure, ExtensionsData extension) {
		authenticate();
		FailedToDeleteData failedToDeleteData = new FailedToDeleteDataImpl();
		return failedToDeleteData;
	}
}
