/*
 * Copyright (C) 2005-2011 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco 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 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco 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 Alfresco. If not, see .
 */
package org.alfresco.repo.publishing;
import static org.alfresco.model.ContentModel.ASSOC_CONTAINS;
import static org.alfresco.repo.publishing.PublishingModel.ASPECT_PUBLISHED;
import static org.alfresco.repo.publishing.PublishingModel.ASSOC_SOURCE;
import static org.alfresco.repo.publishing.PublishingModel.PROP_CHANNEL;
import static org.alfresco.repo.publishing.PublishingModel.PROP_CHANNEL_TYPE;
import static org.alfresco.repo.publishing.PublishingModel.PROP_CHANNEL_TYPE_ID;
import static org.alfresco.repo.publishing.PublishingModel.TYPE_DELIVERY_CHANNEL;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.node.NodeUtils;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.publishing.channels.Channel;
import org.alfresco.service.cmr.publishing.channels.ChannelService;
import org.alfresco.service.cmr.publishing.channels.ChannelType;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
import org.alfresco.util.collections.CollectionUtils;
import org.alfresco.util.collections.Filter;
import org.alfresco.util.collections.Function;
/**
 * @author Nick Smith
 * @since 4.0
 */
public class ChannelHelper
{
    public static final String NAME = "channelHelper";
    private NodeService nodeService;
    private DictionaryService dictionaryService;
    private FileFolderService fileFolderService;
    private PermissionService permissionService;
    private ServiceRegistry serviceRegistry;
    private PublishingEventHelper eventHelper;
    
    public ChannelHelper()
    {
        super();
    }
    
    public ChannelHelper(ServiceRegistry serviceRegistry, PublishingEventHelper eventHelper)
    {
        this.serviceRegistry = serviceRegistry;
        this.eventHelper = eventHelper;
    }
    public NodeRef createChannelNode(NodeRef parent, ChannelType channelType, String channelName,
            Map props)
    {
        QName channelQName = getChannelQName(channelName);
        QName channelNodeType = channelType.getChannelNodeType();
        ChildAssociationRef channelAssoc = 
            nodeService.createNode(parent, ASSOC_CONTAINS, channelQName, channelNodeType, props);
        NodeRef channelNode = channelAssoc.getChildRef();
        // Allow any user to read Channel permissions.
        permissionService.setPermission(channelNode, PermissionService.ALL_AUTHORITIES, PermissionService.READ_ASSOCIATIONS, true);
        return channelNode;
    }
    public Channel buildChannelObject(NodeRef nodeRef, ChannelService channelService)
    {
        if (nodeRef == null || nodeService.exists(nodeRef) == false)
        {
            return null;
        }
        Map props = nodeService.getProperties(nodeRef);
        String channelTypeId = (String) props.get(PROP_CHANNEL_TYPE_ID);
        ChannelType channelType = channelService.getChannelType(channelTypeId);
        String name = (String) props.get(ContentModel.PROP_NAME);
        return new ChannelImpl(serviceRegistry, (AbstractChannelType) channelType, nodeRef, name, this, eventHelper);
    }
    /**
     * Given a noderef from the editorial space (e.g. the doclib), this returns the corresponding noderef published to the specified channel.
     * @param source NodeRef
     * @param channelNode NodeRef
     * @return NodeRef
     */
    public NodeRef mapSourceToEnvironment(NodeRef source, final NodeRef channelNode)
    {
        return mapSourceToEnvironment(source, channelNode, nodeService);
    }
    
    /**
     * Given a noderef from the editorial space (e.g. the doclib), this returns the corresponding noderef published to the specified channel.
     * @param source NodeRef
     * @param channelNode NodeRef
     * @param nodeService NodeService
     * @return NodeRef
     */
    public static NodeRef mapSourceToEnvironment(NodeRef source, final NodeRef channelNode, final NodeService nodeService)
    {
        if (source == null || channelNode == null)
        {
            return null;
        }
        List sourceAssocs = nodeService.getSourceAssocs(source, ASSOC_SOURCE);
        Function super AssociationRef, Boolean> acceptor = new Filter()
        {
            public Boolean apply(AssociationRef assoc)
            {
                NodeRef publishedNode = assoc.getSourceRef();
                NodeRef parent = nodeService.getPrimaryParent(publishedNode).getParentRef();
                return channelNode.equals(parent);
            }
        };
        AssociationRef assoc = CollectionUtils.findFirst(sourceAssocs, acceptor);
        return assoc == null ? null : assoc.getSourceRef();
    }
    
    /**
     * Given a published noderef, this returns the corresponding source noderef in the editorial space (doclib).
     * @param publishedNode NodeRef
     * @return NodeRef
     */
    public NodeRef mapEnvironmentToSource(NodeRef publishedNode)
    {
        return mapEnvironmentToSource(publishedNode, nodeService);
    }
    
    /**
     * Given a published noderef, this returns the corresponding source noderef in the editorial space (doclib).
     * @param publishedNode NodeRef
     * @param nodeService NodeService
     * @return NodeRef
     */
    public static NodeRef mapEnvironmentToSource(NodeRef publishedNode, NodeService nodeService)
    {
        List assocs = nodeService.getTargetAssocs(publishedNode, ASSOC_SOURCE);
        return NodeUtils.getSingleAssocNode(assocs, true);
    }
    /**
     * Finds the {@link Channel} NodeRef and {@link ChannelType} id for a given node, if such a Channel exists.
     * @param node NodeRef
     * @return a {@link Pair} containing the Channel {@link NodeRef} and ChannelType Id.
     */
    public Pair findChannelAndType(NodeRef node)
    {
        Pair result = getChannelAndTypeIfChannel(node);
        if (result == null)
        {
            result = getChannelAndType(node);
            if (result == null)
            {
                ChildAssociationRef parentAssoc = nodeService.getPrimaryParent(node);
                if (parentAssoc != null)
                {
                    NodeRef parent = parentAssoc.getParentRef();
                    if (parent != null)
                    {
                        result = findChannelAndType(parent);
                    }
                }
            }
        }
        return result;
    }
    public Map getChannelProperties(NodeRef channel)
    {
        return nodeService.getProperties(channel);
    }
    
    public AssociationRef createMapping(NodeRef source, NodeRef publishedNode)
    {
        AssociationRef assoc = nodeService.createAssociation(publishedNode, source, ASSOC_SOURCE);
        return assoc;
    }
    public boolean canPublish(NodeRef nodeToPublish, ChannelType type)
    {
        if (type.canPublish() == false)
        {
            return false;
        }
        FileInfo file = fileFolderService.getFileInfo(nodeToPublish);
        ContentData contentData = file.getContentData();
        String mimetype = contentData == null ? null : contentData.getMimetype();
        boolean isContentTypeSupported = isContentTypeSupported(file.getType(), type);
        boolean isMimetypeSupported = isMimetypeSupported(mimetype, type);
        return isContentTypeSupported && isMimetypeSupported;
    }
    private boolean isMimetypeSupported(String mimetype, ChannelType type)
    {
        Set supportedMimetypes = type.getSupportedMimeTypes();
        if (supportedMimetypes == null || supportedMimetypes.isEmpty())
        {
            return true;
        }
        return supportedMimetypes.contains(mimetype);
    }
    private boolean isContentTypeSupported(QName contentType, ChannelType type)
    {
        Set supportedContentTypes = type.getSupportedContentTypes();
        if (supportedContentTypes == null || supportedContentTypes.isEmpty())
        {
            return true;
        }
        for (QName supportedType : supportedContentTypes)
        {
            if (contentType.equals(supportedType) 
                    || dictionaryService.isSubClass(contentType, supportedType))
            {
                return true;
            }
        }
        return false;
    }
    private QName getChannelQName(String channelName)
    {
        return QName.createQName(NamespaceService.APP_MODEL_1_0_URI, channelName);
    }
    private Pair getChannelAndTypeIfChannel(NodeRef node)
    {
        QName type = nodeService.getType(node);
        if (dictionaryService.isSubClass(type, TYPE_DELIVERY_CHANNEL))
        {
            String channelTypeId = (String) nodeService.getProperty(node, PROP_CHANNEL_TYPE_ID);
            if (channelTypeId == null)
            {
                TypeDefinition typeDef = dictionaryService.getType(type);
                PropertyDefinition channelTypeProp = typeDef.getProperties().get(PROP_CHANNEL_TYPE_ID);
                if (channelTypeProp !=null)
                {
                    channelTypeId = channelTypeProp.getDefaultValue();
                }
            }
            return new Pair(node, channelTypeId);
        }
        return null;
    }
    public List getAllChannels(NodeRef channelContainer, final ChannelService channelService)
    {
        List channelAssocs = getChannelAssocs(channelContainer);
        return CollectionUtils.transform(channelAssocs, getChannelTransformer(channelService, false));
    }
    
    public List getChannelsForTypes(final NodeRef containerNode, List types, final ChannelService channelService, final boolean checkPermissions)
    {
        return CollectionUtils.transformFlat(types, new Function>()
        {
            public List apply(ChannelType channelType)
            {
                return getChannelsByType(containerNode, channelType.getId(), channelService, checkPermissions);
            }
        });
    }
    public List getChannelsByType(NodeRef containerNode, String channelTypeId, ChannelService channelService, boolean checkPermissions)
    {
        List channelAssocs = getChannelAssocsByType(containerNode, channelTypeId);
        return CollectionUtils.transform(channelAssocs, getChannelTransformer(channelService, checkPermissions));
    }
    
    public List filterAuthorisedChannels(Collection channels)
    {
        return CollectionUtils.filter(channels, new Filter()
        {
            @Override
            public Boolean apply(Channel value)
            {
                return value.isAuthorised();
            }
        });
    }
    
    public List getReleventChannelTypes(final NodeRef nodeToPublish, Collection channelTypes)
    {
        return CollectionUtils.filter(channelTypes, new Filter()
        {
            public Boolean apply(ChannelType type)
            {
                return canPublish(nodeToPublish, type);
            }
        });
    }
    
    public List getStatusUpdateChannelTypes(Collection channelTypes)
    {
        return CollectionUtils.filter(channelTypes, new Filter()
        {
            public Boolean apply(ChannelType type)
            {
                return type.canPublishStatusUpdates();
            }
        });
    }
    public void addPublishedAspect(NodeRef publishedNode, NodeRef channelNode)
    {
        nodeService.addAspect(publishedNode, ASPECT_PUBLISHED, null);
    }
    private List getChannelAssocs(NodeRef channelContainer)
    {
        if (channelContainer == null)
        {
            return null;
        }
        Collection channelNodeTypes = dictionaryService.getSubTypes(TYPE_DELIVERY_CHANNEL, true);
        HashSet childNodeTypeQNames = new HashSet(channelNodeTypes);
        return nodeService.getChildAssocs(channelContainer, childNodeTypeQNames);
    }
    
    private List getChannelAssocsByType(NodeRef channelContainer, String channelTypeId)
    {
        if (channelContainer == null)
        {
            return null;
        }
        return nodeService.getChildAssocsByPropertyValue(channelContainer, PROP_CHANNEL_TYPE_ID, channelTypeId);
    }
    
    private Pair getChannelAndType(NodeRef node)
    {
        NodeRef channel = (NodeRef) nodeService.getProperty(node, PROP_CHANNEL);
        if (channel != null)
        {
            String channelType = (String) nodeService.getProperty(node, PROP_CHANNEL_TYPE);
            return new Pair(channel, channelType);
        }
        return null;
    }
    
    private Function getChannelTransformer(final ChannelService channelService, final boolean checkPermissions)
    {
        return new Function()
        {
            public Channel apply(ChildAssociationRef value)
            {
                NodeRef channelNode = value.getChildRef();
                if (checkPermissions && hasPublishPermissions(channelNode)==false)
                {
                    return null;
                }
                return buildChannelObject(channelNode, channelService);
            }
        };
    }
    public boolean hasPublishPermissions(NodeRef channelNode)
    {
        AccessStatus access = permissionService.hasPermission(channelNode, PermissionService.ADD_CHILDREN);
        return AccessStatus.ALLOWED == access;
    }
    
    public boolean isChannelAuthorised(NodeRef channelNode)
    {
        Boolean isAuthorised = Boolean.FALSE;
        if (nodeService.exists(channelNode))
        {
            isAuthorised = (Boolean)nodeService.getProperty(channelNode, PublishingModel.PROP_AUTHORISATION_COMPLETE);
        }
        return isAuthorised;
    }
    /**
     * @param nodeService the nodeService to set
     */
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
    
    /**
     * @param dictionaryService the dictionaryService to set
     */
    public void setDictionaryService(DictionaryService dictionaryService)
    {
        this.dictionaryService = dictionaryService;
    }
    /**
     * @param fileFolderService the fileFolderService to set
     */
    public void setFileFolderService(FileFolderService fileFolderService)
    {
        this.fileFolderService = fileFolderService;
    }
    /**
     * @param permissionService the permissionService to set
     */
    public void setPermissionService(PermissionService permissionService)
    {
        this.permissionService = permissionService;
    }
    public void setServiceRegistry(ServiceRegistry serviceRegistry)
    {
        this.serviceRegistry = serviceRegistry;
    }
    public void setEventHelper(PublishingEventHelper eventHelper)
    {
        this.eventHelper = eventHelper;
    }
}