/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * 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 <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.domain.permissions;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
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.domain.node.NodeDAO;
import org.alfresco.repo.domain.node.NodeIdAndAclId;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.permissions.ACLType;
import org.alfresco.repo.security.permissions.AccessControlList;
import org.alfresco.repo.security.permissions.AccessControlListProperties;
import org.alfresco.repo.security.permissions.SimpleAccessControlListProperties;
import org.alfresco.repo.security.permissions.impl.AclChange;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.ConcurrencyFailureException;

/**
 * DAO layer for the improved ACL implementation. This layer is responsible for setting ACLs and any cascade behaviour
 * required. It also implements the migration from the old implementation to the new.
 * 
 * @author andyh
 */
public class ADMAccessControlListDAO implements AccessControlListDAO
{
    private static final Log log = LogFactory.getLog(ADMAccessControlListDAO.class);
    /**
     * The DAO for Nodes.
     */
    private NodeDAO nodeDAO;

    private AclDAO aclDaoComponent;
    
    private BehaviourFilter behaviourFilter;
    private boolean preserveAuditableData = true;
    
    /**maxim transaction time allowed for {@link #setFixedAcls(Long, Long, Long, Long, List, boolean, AsyncCallParameters, boolean)} */
    private long fixedAclMaxTransactionTime = 10 * 1000;

    public void setNodeDAO(NodeDAO nodeDAO)
    {
        this.nodeDAO = nodeDAO;
    }

    public void setAclDAO(AclDAO aclDaoComponent)
    {
        this.aclDaoComponent = aclDaoComponent;
    }
    
    public void setFixedAclMaxTransactionTime(long fixedAclMaxTransactionTime)
    {
        this.fixedAclMaxTransactionTime = fixedAclMaxTransactionTime;
    }
    
    public void setBehaviourFilter(BehaviourFilter behaviourFilter)
    {
        this.behaviourFilter = behaviourFilter;
    }

    public void setPreserveAuditableData(boolean preserveAuditableData)
    {
        this.preserveAuditableData = preserveAuditableData;
    }
    
    public boolean isPreserveAuditableData()
    {
        return preserveAuditableData;
    }

    public void forceCopy(NodeRef nodeRef)
    {
        // Nothing to do
    }

    private Long getNodeIdNotNull(NodeRef nodeRef)
    {
        Pair<Long, NodeRef> nodePair = nodeDAO.getNodePair(nodeRef);
        if (nodePair == null)
        {
            throw new InvalidNodeRefException(nodeRef);
        }
        return nodePair.getFirst();
    }

    public Acl getAccessControlList(NodeRef nodeRef)
    {
        Long nodeId = getNodeIdNotNull(nodeRef);
        Long aclId = nodeDAO.getNodeAclId(nodeId);
        return aclDaoComponent.getAcl(aclId);
    }

    public Acl getAccessControlList(StoreRef storeRef)
    {
        return null;
    }

    public Long getIndirectAcl(NodeRef nodeRef)
    {
        return getAccessControlList(nodeRef).getId();
    }

    public Long getInheritedAcl(NodeRef nodeRef)
    {
        Pair<Long, NodeRef> nodePair = nodeDAO.getNodePair(nodeRef);
        if (nodePair == null)
        {
            return null;
        }
        Pair<Long, ChildAssociationRef> parentAssocRefPair = nodeDAO.getPrimaryParentAssoc(nodePair.getFirst());
        if (parentAssocRefPair == null || parentAssocRefPair.getSecond().getParentRef() == null)
        {
            return null;
        }
        Acl acl = getAccessControlList(parentAssocRefPair.getSecond().getParentRef());
        if (acl != null)
        {
            return acl.getId();
        }
        else
        {
            return null;
        }
    }

    public Map<ACLType, Integer> patchAcls()
    {
        CounterSet result = new CounterSet();
        List<Pair<Long, StoreRef>> stores = nodeDAO.getStores();

        for (Pair<Long, StoreRef> pair : stores)
        {
            CounterSet update;
            Long rootNodeId = nodeDAO.getRootNode(pair.getSecond()).getFirst();
            update = fixOldDmAcls(rootNodeId, nodeDAO.getNodeAclId(rootNodeId), (Long)null, true);
            result.add(update);
        }

        HashMap<ACLType, Integer> toReturn = new HashMap<ACLType, Integer>();
        toReturn.put(ACLType.DEFINING, Integer.valueOf(result.get(ACLType.DEFINING).getCounter()));
        toReturn.put(ACLType.FIXED, Integer.valueOf(result.get(ACLType.FIXED).getCounter()));
        toReturn.put(ACLType.GLOBAL, Integer.valueOf(result.get(ACLType.GLOBAL).getCounter()));
        toReturn.put(ACLType.LAYERED, Integer.valueOf(result.get(ACLType.LAYERED).getCounter()));
        toReturn.put(ACLType.OLD, Integer.valueOf(result.get(ACLType.OLD).getCounter()));
        toReturn.put(ACLType.SHARED, Integer.valueOf(result.get(ACLType.SHARED).getCounter()));
        return toReturn;
    }

    private CounterSet fixOldDmAcls(Long nodeId, Long existingNodeAclId, Long inheritedAclId, boolean isRoot)
    {
        CounterSet result = new CounterSet();
        
        // If existingNodeAclId is not null and equal to inheritedAclId then we know we have hit a shared ACL we have bulk set 
        // - just carry on in this case - we do not need to get the acl
        
        Long newDefiningAcl = null;
        
        if((existingNodeAclId != null) && (existingNodeAclId.equals(inheritedAclId)))
        {
            // nothing to do except move into the children
        }
        else
        {
            AccessControlList existing = null;
            if (existingNodeAclId != null)
            {
                existing = aclDaoComponent.getAccessControlList(existingNodeAclId);
            }

            if (existing != null)
            {
                if (existing.getProperties().getAclType() == ACLType.OLD)
                {
                    result.increment(ACLType.DEFINING);
                    SimpleAccessControlListProperties properties = new SimpleAccessControlListProperties(aclDaoComponent.getDefaultProperties());
                    properties.setInherits(existing.getProperties().getInherits());
                   
                    Long actuallyInherited = null;
                    if (existing.getProperties().getInherits())
                    {
                        if (inheritedAclId != null)
                        {
                            actuallyInherited = inheritedAclId;
                        }
                    }
                    Acl newAcl = aclDaoComponent.createAccessControlList(properties, existing.getEntries(), actuallyInherited);
                    newDefiningAcl = newAcl.getId();
                    nodeDAO.setNodeAclId(nodeId, newDefiningAcl);
                }
                else if (existing.getProperties().getAclType() == ACLType.SHARED)
                {
                    // nothing to do just cascade into the children - we most likely did a bulk set above.
                    // TODO: Check shared ACL set is correct
                }
                else
                {
                    // Already fixed up
                    // TODO: Keep going to check
                    // Check inheritance is correct
                    return result;
                }
            }
            else
            {
                // Set default ACL on roots with no settings
                if (isRoot)
                {
                    result.increment(ACLType.DEFINING);

                    AccessControlListProperties properties = aclDaoComponent.getDefaultProperties();
                    Acl newAcl = aclDaoComponent.createAccessControlList(properties);
                    newDefiningAcl = newAcl.getId();
                    nodeDAO.setNodeAclId(nodeId, newDefiningAcl);
                }
                else
                {
                    // Unset - simple inherit
                    nodeDAO.setNodeAclId(nodeId, inheritedAclId);
                }
            }
        }

        Long toInherit = null;
        List<NodeIdAndAclId> children = nodeDAO.getPrimaryChildrenAcls(nodeId);
        if (children.size() > 0)
        {
            // Only make inherited if required
            if (newDefiningAcl == null)
            {
                toInherit = inheritedAclId;
            }
            else
            {
                toInherit = aclDaoComponent.getInheritedAccessControlList(newDefiningAcl);
            }

        }

        if(children.size() > 0)
        {
            nodeDAO.setPrimaryChildrenSharedAclId(nodeId, null, toInherit);
        }

        for (NodeIdAndAclId child : children)
        {
            CounterSet update = fixOldDmAcls(child.getId(), child.getAclId(), toInherit, false);
            result.add(update);
        }
        
        return result;
    }

    public void setAccessControlList(NodeRef nodeRef, Long aclId)
    {
        boolean auditableBehaviorWasDisabled = preserveAuditableData && behaviourFilter.isEnabled(ContentModel.ASPECT_AUDITABLE);
        if (auditableBehaviorWasDisabled)
        {
            behaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
        }
        
        try
        {
            Long nodeId = getNodeIdNotNull(nodeRef);
            nodeDAO.setNodeAclId(nodeId, aclId);
        }
        finally
        {
            if (auditableBehaviorWasDisabled)
            {
                behaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
            }
        }
    }

    public void setAccessControlList(NodeRef nodeRef, Acl acl)
    {
        Long aclId = null;
        if (acl != null)
        {
            aclId = acl.getId();
        }
        setAccessControlList(nodeRef, aclId);
    }
    
    public void setAccessControlList(StoreRef storeRef, Acl acl)
    {
        throw new UnsupportedOperationException();
    }
    
    public List<AclChange> setInheritanceForChildren(NodeRef parent, Long inheritFrom, Long sharedAclToReplace)
    {
        //check transaction resource to determine if async call may be required 
        boolean asyncCall = AlfrescoTransactionSupport.getResource(FixedAclUpdater.FIXED_ACL_ASYNC_CALL_KEY) == null ? false : true;
        return setInheritanceForChildren(parent, inheritFrom, sharedAclToReplace,  asyncCall);
    }
    
    public List<AclChange> setInheritanceForChildren(NodeRef parent, Long inheritFrom, Long sharedAclToReplace, boolean asyncCall)
    {
        List<AclChange> changes = new ArrayList<AclChange>();
        setFixedAcls(getNodeIdNotNull(parent), inheritFrom, null, sharedAclToReplace, changes, false, asyncCall, true);
        return changes;
    }

    public void updateChangedAcls(NodeRef startingPoint, List<AclChange> changes)
    {
        // Nothing to do: no nodes change as a result of ACL changes
    }

    /**
     * Support to set a shared ACL on a node and all of its children
     * 
     * @param nodeId
     *            the parent node id
     * @param inheritFrom
     *            the parent node's ACL
     * @param mergeFrom
     *            the shared ACL, if already known. If <code>null</code>, will be retrieved / created lazily
     * @param changes
     *            the list in which to record changes
     * @param set
     *            set the shared ACL on the parent ?
     */
    public void setFixedAcls(Long nodeId, Long inheritFrom, Long mergeFrom, Long sharedAclToReplace, List<AclChange> changes, boolean set)
    {
        setFixedAcls(nodeId, inheritFrom, mergeFrom, sharedAclToReplace, changes, set, false, true);
    }
    
    /**
     * Support to set a shared ACL on a node and all of its children
     * 
     * @param nodeId
     *            the parent node
     * @param inheritFrom
     *            the parent node's ACL
     * @param mergeFrom
     *            the shared ACL, if already known. If <code>null</code>, will be retrieved / created lazily
     * @param changes
     *            the list in which to record changes
     * @param set
     *            set the shared ACL on the parent ?
     * @param asyncCall
     *            function may require asynchronous call depending the execution time; if time exceeds configured <code>fixedAclMaxTransactionTime</code> value,
     *            recursion is stopped using propagateOnChildren parameter(set on false) and those nodes for which the method execution was not finished 
     *            in the classical way, will have ASPECT_PENDING_FIX_ACL, which will be used in {@link FixedAclUpdater} for later processing
     */
    public void setFixedAcls(Long nodeId, Long inheritFrom, Long mergeFrom, Long sharedAclToReplace, List<AclChange> changes, boolean set, boolean asyncCall, boolean propagateOnChildren) 
    {
        if (log.isDebugEnabled())
        {
            log.debug(" Set fixed acl for nodeId=" + nodeId + " inheritFrom=" + inheritFrom + " sharedAclToReplace=" + sharedAclToReplace
                    + " mergefrom= " + mergeFrom);
        }
        
        if (nodeId == null)
        {
            return;
        }
        else
        {
            // Lazily retrieve/create the shared ACL
            if (mergeFrom == null)
            {
                mergeFrom = aclDaoComponent.getInheritedAccessControlList(inheritFrom);
            }
            
            if (set)
            {
                nodeDAO.setNodeAclId(nodeId, mergeFrom);
            }
            
            List<NodeIdAndAclId> children = nodeDAO.getPrimaryChildrenAcls(nodeId);
            
            if(children.size() > 0)
            {
                nodeDAO.setPrimaryChildrenSharedAclId(nodeId, sharedAclToReplace, mergeFrom);
            }
            
            if (!propagateOnChildren)
            {
                return;
            }
            for (NodeIdAndAclId child : children)
            {
                Long acl = child.getAclId();
                
                if (acl == null)
                {
                    propagateOnChildren = setFixAclPending(child.getId(), inheritFrom, mergeFrom, sharedAclToReplace, changes, false, asyncCall, propagateOnChildren);
                }
                else
                {
//                    if(acl.equals(mergeFrom))
//                    {
//                        setFixedAcls(child.getId(), inheritFrom, mergeFrom, sharedAclToReplace, changes, false);
//                    }
                    // Already replaced
                    if(acl.equals(sharedAclToReplace))
                    {
                        propagateOnChildren = setFixAclPending(child.getId(), inheritFrom, mergeFrom, sharedAclToReplace, changes, false, asyncCall, propagateOnChildren);
                    }
                    else
                    {
                        Acl dbAcl = aclDaoComponent.getAcl(acl);
                        if (dbAcl.getAclType() == ACLType.LAYERED)
                        {
                            throw new UnsupportedOperationException();
                        }
                        else if (dbAcl.getAclType() == ACLType.DEFINING)
                        {
                            if (dbAcl.getInherits())
                            {
                                @SuppressWarnings("unused")
                                List<AclChange> newChanges = aclDaoComponent.mergeInheritedAccessControlList(mergeFrom, acl);
                            }
                        }
                        else if (dbAcl.getAclType() == ACLType.SHARED)
                        {
                            throw new ConcurrencyFailureException("setFixedAcls: unexpected shared acl: "+dbAcl);
                        }
                    }
                }
            }
        }
    }
    
    /**
     * If async call required adds ASPECT_PENDING_FIX_ACL aspect to nodes when transactionTime reaches max admitted time
     */
    private boolean setFixAclPending(Long nodeId, Long inheritFrom, Long mergeFrom, Long sharedAclToReplace, List<AclChange> changes,
            boolean set, boolean asyncCall, boolean propagateOnChildren)
    {
        // check if async call is required
        if (!asyncCall)
        {
            // make regular method call
            setFixedAcls(nodeId, inheritFrom, mergeFrom, sharedAclToReplace, changes, set, asyncCall, propagateOnChildren);
            return true;
        }
        else
        {
            // check transaction time
            long transactionStartTime = AlfrescoTransactionSupport.getTransactionStartTime();
            long transactionTime = System.currentTimeMillis() - transactionStartTime;

            if (transactionTime < fixedAclMaxTransactionTime)
            {
                // make regular method call if time is under max transaction configured time
                setFixedAcls(nodeId, inheritFrom, mergeFrom, sharedAclToReplace, changes, set, asyncCall, propagateOnChildren);
                return true;
            }
            else
            {
                // time exceeded;
                if (nodeDAO.getPrimaryChildrenAcls(nodeId).size() == 0)
                {
                    // if node is leaf in tree hierarchy call setFixedAcls now as processing with FixedAclUpdater would be more time consuming
                    setFixedAcls(nodeId, inheritFrom, mergeFrom, sharedAclToReplace, changes, set, asyncCall, false);
                }
                else
                {
                    // set ASPECT_PENDING_FIX_ACL aspect on node to be later on processed with FixedAclUpdater
                    addFixedAclPendingAspect(nodeId, sharedAclToReplace, inheritFrom);
                    AlfrescoTransactionSupport.bindResource(FixedAclUpdater.FIXED_ACL_ASYNC_REQUIRED_KEY, true);
                }
                // stop propagating on children nodes
                return false;
            }
        }
    }
    
    private void addFixedAclPendingAspect(Long nodeId, Long sharedAclToReplace, Long inheritFrom)
    {
        Set<QName> aspect = new HashSet<>();
        aspect.add(ContentModel.ASPECT_PENDING_FIX_ACL);
        nodeDAO.addNodeAspects(nodeId, aspect);
        Map<QName, Serializable> pendingAclProperties = new HashMap<>();
        pendingAclProperties.put(ContentModel.PROP_SHARED_ACL_TO_REPLACE, sharedAclToReplace);
        pendingAclProperties.put(ContentModel.PROP_INHERIT_FROM_ACL, inheritFrom);
        nodeDAO.addNodeProperties(nodeId, pendingAclProperties);
        if (log.isDebugEnabled())
        {
            log.debug("Set Fixed Acl Pending : " + nodeId + " " + nodeDAO.getNodePair(nodeId).getSecond());
        }
    }
    
    /**
     * {@inheritDoc}
     */
    public void updateInheritance(Long childNodeId, Long oldParentAclId, Long newParentAclId)
    {
        if (oldParentAclId == null)
        {
            // nothing to do
            return;
        }
        List<AclChange> changes = new ArrayList<AclChange>();
       
        Long childAclId = nodeDAO.getNodeAclId(childNodeId);
        if(childAclId == null)
        { 
            if(newParentAclId != null)
            {
                Long newParentSharedAclId = aclDaoComponent.getInheritedAccessControlList(newParentAclId);
                setFixedAcls(childNodeId, newParentSharedAclId, null, null, changes, true);
            }
        }
        Acl acl = aclDaoComponent.getAcl(childAclId);
        if (acl != null && acl.getInherits())
        {
            Long oldParentSharedAclId = aclDaoComponent.getInheritedAccessControlList(oldParentAclId);
            Long sharedAclchildInheritsFrom = acl.getInheritsFrom();
            if(childAclId.equals(oldParentSharedAclId))
            {
                // child had old shared acl
                if(newParentAclId != null)
                {
                    Long newParentSharedAclId = aclDaoComponent.getInheritedAccessControlList(newParentAclId);
                    setFixedAcls(childNodeId, newParentSharedAclId, null, childAclId, changes, true);
                }
            }
            else if(sharedAclchildInheritsFrom == null)
            {
                // child has defining acl of some form that does not inherit ?
                // Leave alone
            }
            else if(sharedAclchildInheritsFrom.equals(oldParentSharedAclId))
            {
                // child has defining acl and needs to be remerged
                if (acl.getAclType() == ACLType.LAYERED)
                {
                    throw new UnsupportedOperationException();
                }
                else if (acl.getAclType() == ACLType.DEFINING)
                {
                    Long newParentSharedAclId = aclDaoComponent.getInheritedAccessControlList(newParentAclId);
                    @SuppressWarnings("unused")
                    List<AclChange> newChanges = aclDaoComponent.mergeInheritedAccessControlList(newParentSharedAclId, childAclId);
                }
                else if (acl.getAclType() == ACLType.SHARED)
                {
                    throw new IllegalStateException();
                }
            }
            else
            {
                // the acl does not inherit from a node and does not need to be fixed up
                // Leave alone
            }
        }
    }
    
    /**
     * 
     * Counter for each type of ACL change
     * @author andyh
     *
     */
    public static class CounterSet extends HashMap<ACLType, Counter>
    {
        /**
         * 
         */
        private static final long serialVersionUID = -3682278258679211481L;

        CounterSet()
        {
            super();
            this.put(ACLType.DEFINING, new Counter());
            this.put(ACLType.FIXED, new Counter());
            this.put(ACLType.GLOBAL, new Counter());
            this.put(ACLType.LAYERED, new Counter());
            this.put(ACLType.OLD, new Counter());
            this.put(ACLType.SHARED, new Counter());
        }

        void add(ACLType type, Counter c)
        {
            Counter counter = get(type);
            counter.add(c.getCounter());
        }

        void increment(ACLType type)
        {
            Counter counter = get(type);
            counter.increment();
        }

        void add(CounterSet other)
        {
            add(ACLType.DEFINING, other.get(ACLType.DEFINING));
            add(ACLType.FIXED, other.get(ACLType.FIXED));
            add(ACLType.GLOBAL, other.get(ACLType.GLOBAL));
            add(ACLType.LAYERED, other.get(ACLType.LAYERED));
            add(ACLType.OLD, other.get(ACLType.OLD));
            add(ACLType.SHARED, other.get(ACLType.SHARED));
        }
    }

    /**
     * Simple counter
     * @author andyh
     *
     */
    public static class Counter
    {
        int counter;

        void increment()
        {
            counter++;
        }

        int getCounter()
        {
            return counter;
        }

        void add(int i)
        {
            counter += i;
        }
    }
}
