/*
 * Copyright (C) 2017 LibreCCM Foundation.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package com.arsdigita.cms.ui.folder;

import com.arsdigita.kernel.KernelConfig;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.libreccm.categorization.Categorization;
import org.libreccm.categorization.CategoryManager;
import org.libreccm.configuration.ConfigurationManager;
import org.libreccm.core.CcmObject;
import org.libreccm.core.CcmObjectRepository;
import org.libreccm.l10n.GlobalizationHelper;
import org.librecms.CmsConstants;
import org.librecms.contentsection.ContentItem;
import org.librecms.contentsection.ContentItemL10NManager;
import org.librecms.contentsection.ContentItemManager;
import org.librecms.contentsection.ContentItemRepository;
import org.librecms.contentsection.ContentItemVersion;
import org.librecms.contentsection.ContentType;
import org.librecms.contentsection.Folder;
import org.librecms.contentsection.FolderRepository;
import org.librecms.contenttypes.ContentTypeInfo;
import org.librecms.contenttypes.ContentTypesManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;

import java.util.stream.Collectors;

import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Root;
import javax.transaction.Transactional;

import javax.persistence.TypedQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;

import org.librecms.contentsection.FolderManager;

import javax.persistence.criteria.Subquery;

import static org.librecms.CmsConstants.*;

/**
 * The {@code FolderBrowserController} wraps all database operations (queries)
 * required by the {@link FolderBrowser}, the
 * {@link FolderBrowserTableModelBuilder} and the
 * {@link FolderBrowserTableModel}.
 *
 * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
 */
@RequestScoped
public class FolderBrowserController {

    private static final Logger LOGGER = LogManager
        .getLogger(FolderBrowserController.class);

    @Inject
    private EntityManager entityManager;

    @Inject
    private CcmObjectRepository ccmObjectRepo;

    @Inject
    private CategoryManager categoryManager;

    @Inject
    private CcmObjectRepository objectRepo;

    @Inject
    private ConfigurationManager confManager;

    @Inject
    private GlobalizationHelper globalizationHelper;

    @Inject
    private ContentItemRepository itemRepo;

    @Inject
    private ContentItemManager itemManager;

    @Inject
    private ContentItemL10NManager itemL10NManager;

    @Inject
    private ContentTypesManager typesManager;

    @Inject
    private FolderRepository folderRepo;

    @Inject
    private FolderManager folderManager;

    private Locale defaultLocale;

    /**
     * Initialisation method called by the CDI-Container after an instance of
     * this class has be created by the container. Sets the
     * {@link #defaultLocale} property using the the value from the
     * {@link KernelConfig}.
     */
    @PostConstruct
    private void init() {
        final KernelConfig kernelConfig = confManager.findConfiguration(
            KernelConfig.class);
        defaultLocale = kernelConfig.getDefaultLocale();
    }

    /**
     * Count the objects (subfolders and content items) in the provided folder.
     *
     * @param folder The folder.
     *
     * @return The number of objects (subfolders and content items) in the
     *         provided {@code folder}.
     */
    public long countObjects(final Folder folder) {
        return countObjects(folder, "%");
    }

    /**
     * Count all objects (subfolders and content items) in the provided folder
     * which match the provided filter term.
     *
     * @param folder     The folder.
     * @param filterTerm The filter term.
     *
     * @return The number of objects (subfolders and content items) in the
     *         provided {@code folder} which match the provided
     *         {@code filterTerm}.
     */
    public long countObjects(final Folder folder,
                             final String filterTerm) {

        Objects.requireNonNull(folder, "Can't count objects in Folder null.");
        Objects.requireNonNull(filterTerm, "Can't filter for 'null'.");

        final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Long> criteriaQuery = builder.createQuery(Long.class);
        final Root<CcmObject> from = criteriaQuery.from(CcmObject.class);

        criteriaQuery = criteriaQuery.select(builder.count(from));

        final List<Folder> subFolders = findSubFolders(folder,
                                                       filterTerm,
                                                       FolderBrowser.SORT_KEY_NAME,
                                                       FolderBrowser.SORT_ACTION_UP,
                                                       -1,
                                                       -1);
        final List<ContentItem> items = findItemsInFolder(folder,
                                                          filterTerm,
                                                          FolderBrowser.SORT_KEY_NAME,
                                                          FolderBrowser.SORT_ACTION_UP,
                                                          -1,
                                                          -1);
        if (subFolders.isEmpty() && items.isEmpty()) {
            return 0;
        } else if (subFolders.isEmpty() && !items.isEmpty()) {
            criteriaQuery = criteriaQuery.where(from.in(items));
        } else if (!subFolders.isEmpty() && items.isEmpty()) {
            criteriaQuery = criteriaQuery.where(from.in(subFolders));
        } else {
            criteriaQuery = criteriaQuery.where(builder.or(
                from.in(subFolders),
                from.in(items)));
        }

        return entityManager.createQuery(criteriaQuery).getSingleResult();
    }

    /**
     * Create {@link FolderBrowserTableRow} objects for all objects in the
     * provided folder.
     *
     * @param folder         The folder which contains the objects.
     * @param orderBy        The field used to order the objects.
     * @param orderDirection The direction for ordering the objects.
     *
     * @return A list with {@link FolderBrowserTableRow} objects for each object
     *         in the provided {@code folder} ordered by the provided field and
     *         in the provided direction.
     */
    @Transactional(Transactional.TxType.REQUIRED)
    List<FolderBrowserTableRow> getObjectRows(final Folder folder,
                                              final String orderBy,
                                              final String orderDirection) {
        return getObjectRows(folder, "%", orderBy, orderDirection, -1, -1);
    }

    /**
     * Create {@link FolderBrowserTableRow} objects for all objects in the
     * provided folder which match provided filter term.
     *
     * @param folder         The folder which contains the objects.
     * @param filterTerm     The filter term.
     * @param orderBy        The field used to order the objects.
     * @param orderDirection The direction for ordering the objects.
     *
     * @return A list with {@link FolderBrowserTableRow} objects for each object
     *         in the provided {@code folder} which matches the provided
     *         {@code filterTerm}, ordered by the provided field and in the
     *         provided direction.
     */
    @Transactional(Transactional.TxType.REQUIRED)
    List<FolderBrowserTableRow> getObjectRows(final Folder folder,
                                              final String filterTerm,
                                              final String orderBy,
                                              final String orderDirection) {
        return getObjectRows(folder,
                             filterTerm,
                             orderBy,
                             orderDirection,
                             -1,
                             -1);
    }

    /**
     * Create {@link FolderBrowserTableRow} objects for the objects in the
     * provided folder which are in the range provided by {@code firstResult}
     * and {@code maxResult}
     *
     * @param folder         The folder which contains the objects.
     * @param orderBy        The field used to order the objects.
     * @param orderDirection The direction for ordering the objects.
     * @param firstResult    The index of the first object to use.
     * @param maxResults     The maximum number of objects to retrieve.
     *
     * @return A list with {@link FolderBrowserTableRow} objects for each object
     *         in the provided {@code folder} ordered by the provided field and
     *         in the provided direction. The list will start with the object
     *         with index provided as {@code firstResult} and contain at most
     *         {@code maxResults} items.
     */
    @Transactional(Transactional.TxType.REQUIRED)
    List<FolderBrowserTableRow> getObjectRows(final Folder folder,
                                              final String orderBy,
                                              final String orderDirection,
                                              final int firstResult,
                                              final int maxResults) {
        return getObjectRows(folder,
                             "%",
                             orderBy,
                             orderDirection,
                             firstResult,
                             maxResults);
    }

    /**
     * Create {@link FolderBrowserTableRow} objects for the objects in the
     * provided folder which match the provided filter term and which are in the
     * range provided by {@code firstResult} and {@code maxResult}
     *
     * @param folder         The folder which contains the objects.
     * @param filterTerm     The filter term.
     * @param orderBy        The field used to order the objects.
     * @param orderDirection The direction for ordering the objects.
     * @param firstResult    The index of the first object to use.
     * @param maxResults     The maximum number of objects to retrieve.
     *
     * @return A list with {@link FolderBrowserTableRow} objects for each object
     *         in the provided {@code folder} which matches the provided
     *         {@code filterTerm}, ordered by the provided field and in the
     *         provided direction. The list will start with the object with
     *         index provided as {@code firstResult} and contain at most
     *         {@code maxResults} items.
     */
    @Transactional(Transactional.TxType.REQUIRED)
    List<FolderBrowserTableRow> getObjectRows(final Folder folder,
                                              final String filterTerm,
                                              final String orderBy,
                                              final String orderDirection,
                                              final int firstResult,
                                              final int maxResults) {

        final List<CcmObject> objects = findObjectsInFolder(folder,
                                                            filterTerm,
                                                            orderBy,
                                                            orderDirection,
                                                            firstResult,
                                                            maxResults);
        LOGGER.debug("Found {} objects for folder \"{}\".",
                     objects.size(),
                     folder.getDisplayName());
        
        final List<FolderBrowserTableRow> rows = objects
        .stream()
        .map(object -> buildRow(object))
        .collect(Collectors.toList());
        
        return rows;

//        final List<Folder> subFolders = findSubFolders(folder,
//                                                       filterTerm,
//                                                       orderBy,
//                                                       orderDirection,
//                                                       firstResult,
//                                                       maxResults);
//        final List<FolderBrowserTableRow> subFolderRows = subFolders
//            .stream()
//            .map(subFolder -> buildRow(subFolder))
//            .collect(Collectors.toList());
//
//        if (subFolders.size() >= maxResults) {
//            return subFolderRows;
//        } else {
//        final int maxItems = maxResults - subFolders.size();
//        final int firstItem = firstResult - subFolders.size();
//
//        final List<ContentItem> items = findItemsInFolder(folder,
//                                                          filterTerm,
//                                                          orderBy,
//                                                          orderDirection,
//                                                          firstItem,
//                                                          maxItems);
//        final List<FolderBrowserTableRow> itemRows = items.stream()
//            .map(item -> buildRow(item))
//            .collect(Collectors.toList());
//
//        final List<FolderBrowserTableRow> rows = new ArrayList<>();
//        rows.addAll(subFolderRows);
//        rows.addAll(itemRows);
//
//        return rows;
//        }
    }

    private FolderBrowserTableRow buildRow(final CcmObject object) {
        
        if (object instanceof Folder) {
            return buildRow((Folder) object);
        } else if(object instanceof ContentItem) {
            return buildRow((ContentItem) object);
        } else{
            throw new IllegalArgumentException(
                "Only Folders and ContentItems are supported.");
        }
    }
    
    /**
     * Helper method for building a {@link FolderBrowserTableRow} from a
     * {@link Folder}.
     *
     * @param folder The {@link Folder} to use for building the
     *               {@link FolderBrowserTableRow}.
     *
     * @return A {@link FolderBrowserTableRow} containing the data needed by the
     *         {@link FolderBrowser} to display the provided {@code folder}.
     */
    private FolderBrowserTableRow buildRow(final Folder folder) {

        final FolderBrowserTableRow row = new FolderBrowserTableRow();

        row.setObjectId(folder.getObjectId());
        row.setObjectUuid(folder.getUuid());
        row.setName(folder.getName());
        row.setLanguages(Collections.emptyList());
        if (folder.getTitle().hasValue(globalizationHelper
            .getNegotiatedLocale())) {
            row.setTitle(folder.getTitle().getValue(globalizationHelper
                .getNegotiatedLocale()));
        } else {
            row.setTitle(folder.getTitle().getValue(defaultLocale));
        }
        row.setFolder(true);
        row.setDeletable(!categoryManager.hasSubCategories(folder)
                             && !categoryManager.hasObjects(folder));

        return row;
    }

    /**
     * Helper method for building a {@link FolderBrowserTableRow} from a
     * {@link ContentItem}.
     *
     * @param item The {@link ContentItem} to use for building the
     *             {@link FolderBrowserTableRow}.
     *
     * @return A {@link FolderBrowserTableRow} containing the data needed by the
     *         {@link FolderBrowser} to display the provided {@code item}.
     */
    private FolderBrowserTableRow buildRow(final ContentItem item) {

        final FolderBrowserTableRow row = new FolderBrowserTableRow();

        final Locale negotiatedLocale = globalizationHelper
            .getNegotiatedLocale();

        row.setObjectId(item.getObjectId());
        row.setObjectUuid(item.getItemUuid());

        if (item.getName().hasValue(negotiatedLocale)) {
            row.setName(item.getName().getValue(negotiatedLocale));
        } else {
            row.setName(item.getName().getValue(defaultLocale));
        }
        final List<Locale> languages = new ArrayList<>(itemL10NManager
            .availableLanguages(item));
        languages.sort((lang1, lang2) -> lang1.toString().compareTo(
            lang2.toString()));
        row.setLanguages(languages);
        if (item.getTitle().hasValue(negotiatedLocale)) {
            row.setTitle(item.getTitle().getValue(negotiatedLocale));
        } else {
            row.setTitle(item.getTitle().getValue(defaultLocale));
        }
        final ContentType type = item.getContentType();
        final ContentTypeInfo typeInfo = typesManager.getContentTypeInfo(
            type);
        row.setTypeLabelBundle(typeInfo.getLabelBundle());
        row.setTypeLabelKey(typeInfo.getLabelKey());

        row.setDeletable(!itemManager.isLive(item));

        row.setCreated(item.getCreationDate());
        row.setLastModified(item.getLastModified());

        row.setFolder(false);

        return row;
    }

    /**
     * Called by the {@link FolderBrowser} to delete an object.
     *
     * @param objectId
     */
    @Transactional(Transactional.TxType.REQUIRED)
    protected void deleteObject(final String objectId) {

        Objects.requireNonNull(objectId);

        if (objectId.startsWith("folder-")) {
            final long folderId = Long.parseLong(
                objectId.substring("folder-".length()));

            folderRepo
                .findById(folderId)
                .ifPresent(folderRepo::delete);
        } else if (objectId.startsWith("item-")) {
            final long itemId = Long.parseLong(
                objectId.substring("item-".length()));

            itemRepo
                .findById(itemId)
                .ifPresent(itemRepo::delete);
        } else {
            throw new IllegalArgumentException(
                "The objectId is expected to start with 'folder-' or 'item.'.");
        }
    }

    private List<CcmObject> findObjectsInFolder(final Folder folder,
                                                final String filterTerm,
                                                final String orderBy,
                                                final String orderDirection,
                                                final int firstResult,
                                                final int maxResults) {

        final CriteriaBuilder builder = entityManager.getCriteriaBuilder();

        final CriteriaQuery<CcmObject> criteriaQuery = builder
            .createQuery(CcmObject.class);
        final Root<CcmObject> from = criteriaQuery.from(CcmObject.class);
        criteriaQuery.select(from);

        final Subquery<Folder> folderCriteriaQuery = criteriaQuery
            .subquery(Folder.class);
        final Root<Folder> fromFolder = folderCriteriaQuery.from(Folder.class);
        folderCriteriaQuery.select(fromFolder);
        folderCriteriaQuery.where(
            builder.and(
                builder.equal(fromFolder.get("parentCategory"), folder),
                builder.like(builder.lower(fromFolder.get("name")),
                             filterTerm)));

        final Subquery<ContentItem> itemCriteriaQuery = criteriaQuery
            .subquery(ContentItem.class);
        final Root<ContentItem> fromItem = itemCriteriaQuery
            .from(ContentItem.class);
        itemCriteriaQuery.select(fromItem);
        final Join<ContentItem, Categorization> itemCategoryJoin = fromItem
            .join("categories");
        itemCriteriaQuery.where(builder
            .and(
                builder.equal(itemCategoryJoin.get("category"), folder),
                builder.equal(itemCategoryJoin.get("type"),
                              CmsConstants.CATEGORIZATION_TYPE_FOLDER),
                builder.equal(fromItem.get("version"),
                              ContentItemVersion.DRAFT),
                builder.like(builder.lower(fromItem.get("displayName")),
                             filterTerm)));

        criteriaQuery.where(
            builder.or(
                builder.in(from).value(folderCriteriaQuery),
                builder.in(from).value(itemCriteriaQuery)
            ));

        criteriaQuery.orderBy(builder.asc(from.get("displayName")));
        
        final TypedQuery<CcmObject> query = entityManager
            .createQuery(criteriaQuery);

        if (maxResults > 0) {
            query.setMaxResults(maxResults);
        }
        if (firstResult > 0) {
            query.setFirstResult(firstResult);
        }

        return query.getResultList();
    }

    /**
     * Retrieves all subfolders of a folder matching the provided filter term.
     * Because {@link Folder} does not have any of the fields despite the name
     * which can be used to order the objects ordering is done as follows:
     *
     * If {@code orderBy} has any value other than
     * {@link FolderBrowser#SORT_KEY_NAME} the subfolders are ordered in
     * ascending order by their name. If {@code orderBy} is
     * {@link FolderBrowser#SORT_KEY_NAME} the subfolders are ordered by their
     * name in ascending and descending order depending on the value of
     * {@code orderDirection}.
     *
     * @param folder         The folder which contains the subfolders.
     * @param filterTerm     The filter term.
     * @param orderBy        Field to use for ordering. If the value is negative
     *                       the parameter is ignored.
     * @param orderDirection Direction for ordering. If the value is negative
     *                       the parameter is ignored.
     * @param firstResult    Index of the first result to retrieve.
     * @param maxResults     Maxium number of results to retrieve.
     *
     *
     * @return A list of the subfolders of the provided {@code folder} which
     *         match the provided {@code filterTerm}. The list is ordered as
     *         described above. The list will contain at most {@code maxResults}
     *         starting with the result with the index provided as
     *         {@code firstResult}.
     */
    private List<Folder> findSubFolders(final Folder folder,
                                        final String filterTerm,
                                        final String orderBy,
                                        final String orderDirection,
                                        final int firstResult,
                                        final int maxResults) {

        final CriteriaBuilder builder = entityManager.getCriteriaBuilder();

        final CriteriaQuery<Folder> criteria = builder
            .createQuery(Folder.class);
        final Root<Folder> from = criteria.from(Folder.class);

        final Order order;
        if (FolderBrowser.SORT_KEY_NAME.equals(orderBy)
                && FolderBrowser.SORT_ACTION_DOWN.equals(orderDirection)) {
            order = builder.desc(from.get("name"));
        } else {
            order = builder.asc(from.get("name"));
        }

        final TypedQuery<Folder> query = entityManager
            .createQuery(
                criteria.where(
                    builder.and(
                        builder.equal(from.get("parentCategory"), folder),
                        builder
                            .like(builder.lower(from.get("name")),
                                  filterTerm)
                    )
                )
                    .orderBy(order)
            );

        if (firstResult >= 0) {
            query.setFirstResult(firstResult);
        }
        if (maxResults >= 0) {
            query.setMaxResults(maxResults);
        }

        return query.getResultList();
    }

    /**
     * Retrieves all items of a folder matching the provided filter term.
     *
     * @param folder         The folder which contains the subfolders.
     * @param filterTerm     The filter term.
     * @param orderBy        Field to use for ordering. If the value is negative
     *                       the parameter is ignored.
     * @param orderDirection Direction for ordering. If the value is negative
     *                       the parameter is ignored.
     * @param firstResult    Index of the first result to retrieve.
     * @param maxResults     Maxium number of results to retrieve.
     *
     *
     * @return A list of the subfolders of the provided {@code folder} which
     *         match the provided {@code filterTerm}. The list is ordered the
     *         field provided as {@code orderBy} in the direction provided by
     *         {@code orderDirection}. The list will contain at most
     *         {@code maxResults} starting with the result with the index
     *         provided as {@code firstResult}.
     */
    private List<ContentItem> findItemsInFolder(final Folder folder,
                                                final String filterTerm,
                                                final String orderBy,
                                                final String orderDirection,
                                                final int firstResult,
                                                final int maxResults) {

        final CriteriaBuilder builder = entityManager.getCriteriaBuilder();

        final CriteriaQuery<ContentItem> criteria = builder
            .createQuery(ContentItem.class);
        final Root<ContentItem> fromItem = criteria.from(ContentItem.class);
        final Join<ContentItem, Categorization> join = fromItem
            .join("categories");

        final Path<?> orderPath;
        switch (orderBy) {
            case FolderBrowser.SORT_KEY_NAME:
                orderPath = fromItem.get("displayName");
                break;
            case FolderBrowser.SORT_KEY_CREATION_DATE:
                orderPath = fromItem.get("creationDate");
                break;
            case FolderBrowser.SORT_KEY_LAST_MODIFIED_DATE:
                orderPath = fromItem.get("lastModifed");
                break;
            default:
                orderPath = fromItem.get("displayName");
                break;
        }

        final Order order;
        if (FolderBrowser.SORT_ACTION_DOWN.equals(orderDirection)) {
            order = builder.desc(orderPath);
        } else {
            order = builder.asc(orderPath);
        }

        final TypedQuery<ContentItem> query = entityManager
            .createQuery(
                criteria.select(fromItem)
                    .where(
                        builder.and(
                            builder.equal(join.get("category"), folder),
                            builder.equal(join.get("type"),
                                          CmsConstants.CATEGORIZATION_TYPE_FOLDER),
                            builder.equal(fromItem.get("version"),
                                          ContentItemVersion.DRAFT),
                            builder.like(builder.lower(fromItem.get(
                                "displayName")),
                                         filterTerm)
                        )
                    )
                    .orderBy(order)
            );

        if (firstResult >= 0) {
            query.setFirstResult(firstResult);
        }
        if (maxResults >= 0) {
            query.setMaxResults(maxResults);
        }

        return query.getResultList();
    }

    /**
     * Check if a folder or its subfolders contains at least one live item.
     *
     * @param folder The folder to check for live items.
     *
     * @return {@code true} if the {@code folder} or on of its subfolders
     *         contains at least one live item, {@code false} otherwise.
     */
    @Transactional(Transactional.TxType.REQUIRED)
    public boolean hasLiveItems(final Folder folder) {

        Objects.requireNonNull(folder,
                               "Folder to check for live items can't be null.");

        //Ensure that we use an non detached entity.
        final Folder theFolder = folderRepo.findById(folder.getObjectId())
            .orElseThrow(() -> new IllegalArgumentException(String.format(
            "No folder with id %s in the database. Where did that ID come from?",
            folder.getObjectId())));

        final boolean hasLiveItem = theFolder.getObjects()
            .stream()
            .map(categorization -> categorization.getCategorizedObject())
            .filter(object -> object instanceof ContentItem)
            .map(object -> (ContentItem) object)
            .filter(item -> itemManager.isLive(item))
            .anyMatch(item -> itemManager.isLive(item));

        if (hasLiveItem) {
            return true;
        } else {
            return theFolder.getSubFolders()
                .stream()
                .anyMatch(currentFolder -> hasLiveItems(currentFolder));
        }
    }

    /**
     * Get the IDs of the folders invalid for copy/move actions.
     *
     * @param sources
     *
     * @return
     */
    @Transactional(Transactional.TxType.REQUIRED)
    public List<String> createInvalidTargetsList(final List<String> sources) {

        Objects.requireNonNull(sources);

        final List<String> sourceFolderIds = sources
            .stream()
            .filter(source -> source.startsWith(
            FOLDER_BROWSER_KEY_PREFIX_FOLDER))
            .collect(Collectors.toList());
        final List<String> parentFolderIds = sourceFolderIds
            .stream()
            .map(sourceFolderId -> findParentFolderId(sourceFolderId))
            .filter(Optional::isPresent)
            .map(Optional::get)
            .collect(Collectors.toList());
        final List<List<String>> subFolderIds = sourceFolderIds
            .stream()
            .map(sourceFolderId -> findSubFolderIds(sourceFolderId))
            .collect(Collectors.toList());

        final List<String> invalidTargetIds = new ArrayList<>();
        invalidTargetIds.addAll(sourceFolderIds);
        invalidTargetIds.addAll(parentFolderIds);
        for (final List<String> subFolderIdList : subFolderIds) {
            invalidTargetIds.addAll(subFolderIdList);
        }

        return invalidTargetIds;
    }

    private Optional<String> findParentFolderId(final String folderId) {

        Objects.requireNonNull(folderId);

        if (!folderId.startsWith(FOLDER_BROWSER_KEY_PREFIX_FOLDER)) {
            throw new IllegalArgumentException(String.format(
                "Provided string '%s' is not an ID of a folder.",
                folderId));
        }

        final long objectId = Long.parseLong(folderId.substring(
            FOLDER_BROWSER_KEY_PREFIX_FOLDER.length()));
        final Folder folder = folderRepo.findById(objectId)
            .orElseThrow(() -> new IllegalArgumentException(String.format(
            "No folder with ID %d found in database. "
                + "Where did that ID come form?",
            objectId)));
        final Optional<Folder> parentFolder = folderManager.getParentFolder(
            folder);
        if (parentFolder.isPresent()) {
            return Optional.empty();
        } else {
            return Optional.ofNullable(String.format(
                "%s%d",
                FOLDER_BROWSER_KEY_PREFIX_FOLDER,
                parentFolder.get().getObjectId()));
        }
    }

    private List<String> findSubFolderIds(final String folderId) {

        Objects.requireNonNull(folderId);

        if (!folderId.startsWith(FOLDER_BROWSER_KEY_PREFIX_FOLDER)) {
            throw new IllegalArgumentException(String.format(
                "Provided string '%s' is not the ID of a folder.",
                folderId));
        }

        final long objectId = Long.parseLong(folderId.substring(
            FOLDER_BROWSER_KEY_PREFIX_FOLDER.length()));
        final Folder folder = folderRepo.findById(objectId)
            .orElseThrow(() -> new IllegalArgumentException(String.format(
            "No folder with ID %d found in database. "
                + "Where did that ID come form?",
            objectId)));
        return findSubFolders(folder)
            .stream()
            .map(subFolder -> String.format("%s%d",
                                            FOLDER_BROWSER_KEY_PREFIX_FOLDER,
                                            subFolder.getObjectId()))
            .collect(Collectors.toList());
    }

    private List<Folder> findSubFolders(final Folder folder) {

        Objects.requireNonNull(folder);

        if (folder.getSubFolders() == null
                || folder.getSubFolders().isEmpty()) {
            return Collections.emptyList();
        }

        final List<Folder> subFolders = new ArrayList<>();
        for (final Folder subFolder : folder.getSubFolders()) {
            subFolders.add(subFolder);
            subFolders.addAll(findSubFolders(subFolder));
        }

        return subFolders;
    }

    @Transactional(Transactional.TxType.REQUIRED)
    public void moveObjects(final Folder targetFolder,
                            final String[] objectIds) {

        Objects.requireNonNull(targetFolder);
        Objects.requireNonNull(objectIds);

        for (final String objectId : objectIds) {
            if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_FOLDER)) {
                moveFolder(targetFolder,
                           Long.parseLong(objectId.substring(
                               FOLDER_BROWSER_KEY_PREFIX_FOLDER.length())));
            } else if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_ITEM)) {
                moveItem(targetFolder,
                         Long.parseLong(objectId.substring(
                             FOLDER_BROWSER_KEY_PREFIX_ITEM.length())));
            } else {
                throw new IllegalArgumentException(String.format(
                    "ID '%s' does not start with '%s' or '%s'.",
                    objectId,
                    FOLDER_BROWSER_KEY_PREFIX_FOLDER,
                    FOLDER_BROWSER_KEY_PREFIX_ITEM));
            }
        }
    }

    private void moveFolder(final Folder targetFolder, final long folderId) {

        Objects.requireNonNull(targetFolder);

        final Folder folder = folderRepo.findById(folderId)
            .orElseThrow(() -> new IllegalArgumentException(String.format(
            "No folder with ID %d in the database. "
                + "Where did that ID come from?",
            folderId)));

        folderManager.moveFolder(folder, targetFolder);
    }

    private void moveItem(final Folder targetFolder, final long itemId) {

        Objects.requireNonNull(targetFolder);

        final ContentItem item = itemRepo
            .findById(itemId)
            .orElseThrow(() -> new IllegalArgumentException(String.format(
            "No content item with ID %d in the database. "
                + "Where did that ID come from?",
            itemId)));

        itemManager.move(item, targetFolder);
    }

    @Transactional(Transactional.TxType.REQUIRED)
    public void copyObjects(final Folder targetFolder,
                            final String[] objectIds) {

        Objects.requireNonNull(targetFolder);
        Objects.requireNonNull(objectIds);

        for (final String objectId : objectIds) {
            if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_FOLDER)) {
                copyFolder(targetFolder,
                           Long.parseLong(objectId.substring(
                               FOLDER_BROWSER_KEY_PREFIX_FOLDER.length())));
            } else if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_ITEM)) {
                copyItem(targetFolder,
                         Long.parseLong(objectId.substring(
                             FOLDER_BROWSER_KEY_PREFIX_ITEM.length())));
            } else {
                throw new IllegalArgumentException(String.format(
                    "ID '%s' does not start with '%s' or '%s'.",
                    objectId,
                    FOLDER_BROWSER_KEY_PREFIX_FOLDER,
                    FOLDER_BROWSER_KEY_PREFIX_ITEM));
            }
        }
    }

    private void copyFolder(final Folder targetFolder,
                            final long folderId) {

        Objects.requireNonNull(targetFolder);

        final Folder folder = folderRepo.findById(folderId)
            .orElseThrow(() -> new IllegalArgumentException(String.format(
            "No folder with ID %d in the database. "
                + "Where did that ID come from?",
            folderId)));

        folderManager.copyFolder(folder, targetFolder);
    }

    private void copyItem(final Folder targetFolder,
                          final long itemId) {

        Objects.requireNonNull(targetFolder);

        final ContentItem item = itemRepo
            .findById(itemId)
            .orElseThrow(() -> new IllegalArgumentException(String.format(
            "No content item with ID %d in the database. "
                + "Where did that ID come from?",
            itemId)));

        itemManager.copy(item, targetFolder);
    }

}
