/*
 * Copyright (C) 2005-2007 Alfresco Software Limited.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.

 * 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 General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

 * As a special exception to the terms and conditions of version 2.0 of 
 * the GPL, you may redistribute this Program in connection with Free/Libre 
 * and Open Source Software ("FLOSS") applications as described in Alfresco's 
 * FLOSS exception.  You should have recieved a copy of the text describing 
 * the FLOSS exception, and it is also available here: 
 * http://www.alfresco.com/legal/licensing
 */

package org.alfresco.repo.simple.permission;

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.repo.avm.util.RawServices;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.simple.permission.ACL;
import org.alfresco.service.simple.permission.AuthorityCapabilityRegistry;

/**
 * Basic implementation of a simple ACL.
 * @author britt
 */
public class ACLImpl implements ACL
{
    private static final long serialVersionUID = -8720314753104805631L;
    
    /**
     * Map of capabilities to authorities allowed.
     */
    private Map<String, Set<String>> fAllowed;
    
    /**
     * Map of capabilities to authorities denied.
     */
    private Map<String, Set<String>> fDenied;

    /**
     * Should this ACL be inherited.
     */
    private boolean fInherit;
    
    /**
     * String (compact) representation of ACL.
     */
    private String fStringRep;
    
    /**
     * Reference to the capability registry.
     */
    private transient AuthorityCapabilityRegistry fCapabilityRegistry;
    
    /**
     * Initialize a brand new one.
     * @param inherit Should this ACL be inherited.
     */
    public ACLImpl(boolean inherit)
    {
        fInherit = inherit;
        fCapabilityRegistry = RawServices.Instance().getAuthorityCapabilityRegistry();
        fAllowed = new HashMap<String, Set<String>>();
        fDenied = new HashMap<String, Set<String>>();
        fStringRep = null;
    }
    
    /**
     * Initialize from an external string representation.
     * @param rep
     */
    public ACLImpl(String rep)
    {
        this(true);
        fStringRep = rep;
    }
    
    public ACLImpl(ACL other)
    {
        this(true);
        fStringRep = other.getStringRepresentation();
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.service.simple.permission.ACL#allow(java.lang.String, java.lang.String[])
     */
    public void allow(String capability, String... authorities)
    {
        capability = capability.toLowerCase();
        List<String> auths = new ArrayList<String>();
        for (String auth : authorities)
        {
            auths.add(fCapabilityRegistry.normalizeAuthority(auth));
        }
        digest();
        // First remove any explicit denies.
        Set<String> denied = fDenied.get(capability);
        if (denied != null)
        {
            for (String authority : auths)
            {
                denied.remove(authority);
            }
        }
        // Add the authorities to the allowed list.
        Set<String> allowed = fAllowed.get(capability);
        if (allowed == null)
        {
            allowed = new HashSet<String>();
            fAllowed.put(capability, allowed);
        }
        for (String authority : auths)
        {
            allowed.add(authority);
        }
    }

    /**
     * Helper to decode from the string representation.
     */
    private void digest()
    {
        if (fStringRep == null)
        {
            return;
        }
        String[] segments = fStringRep.split("\\|");
        fInherit = segments[0].equals("i");
        digestMap(segments[1], fAllowed);
        digestMap(segments[2], fDenied);
        fStringRep = null;
    }

    /**
     * Sub helper for decoding string representation.
     * @param string The partial string representation.
     * @param map The map to update.
     */
    private void digestMap(String rep, Map<String, Set<String>> map)
    {
        String[] segments = rep.split(":");
        if (segments.length == 0 || segments[0].equals(""))
        {
            // This means there are no explicit entries.
            return;
        }
        for (String entryRep : segments)
        {
            String[] entryRegs = entryRep.split(";");
            String capability = fCapabilityRegistry.getCapabilityName(Integer.parseInt(entryRegs[0], 32));
            if (capability == null)
            {
                continue;
            }
            Set<String> authorities = new HashSet<String>();
            map.put(capability, authorities);
            for (int i = 1; i < entryRegs.length; ++i)
            {
                String authority = fCapabilityRegistry.getAuthorityName(Integer.parseInt(entryRegs[i], 32));
                if (authority == null)
                {
                    continue;
                }
                authorities.add(authority);
            }
        }
    }

    /* (non-Javadoc)
     * @see org.alfresco.service.simple.permission.ACL#can(java.lang.String, boolean, java.lang.String)
     */
    public boolean can(String authority, boolean isOwner, String capability)
    {
        authority = fCapabilityRegistry.normalizeAuthority(authority);
        capability = capability.toLowerCase();
        digest();
        AuthorityType type = AuthorityType.getAuthorityType(authority);
        // Admin trumps.
        if (type == AuthorityType.ADMIN)
        {
            return true;
        }
        // Look for denies first.
        Set<String> denied = fDenied.get(capability);
        if (denied != null)
        {
            if (denied.contains(authority))
            {
                return false;
            }
            for (String auth : denied)
            {
                if (fCapabilityRegistry.getContainedAuthorities(auth).contains(authority))
                {
                    return false;
                }
            }
        }
        // Now look for allows.
        Set<String> allowed = fAllowed.get(capability);
        if (allowed != null)
        {
            if (allowed.contains(authority))
            {
                return true;
            }
            for (String auth : allowed)
            {
                if (fCapabilityRegistry.getContainedAuthorities(auth).contains(authority))
                {
                    return true;
                }
            }
        }
        return false;
    }

    /* (non-Javadoc)
     * @see org.alfresco.service.simple.permission.ACL#deny(java.lang.String, java.lang.String[])
     */
    public void deny(String capability, String ... authorities)
    {
        capability = capability.toLowerCase();
        List<String> auths = new ArrayList<String>();
        for (String auth : authorities)
        {
            auths.add(fCapabilityRegistry.normalizeAuthority(auth));
        }
        digest();
        // Remove corresponding explicit allows.
        Set<String> allowed = fAllowed.get(capability);
        if (allowed != null)
        {
            for (String authority : auths)
            {
                allowed.remove(authority);
            }
        }
        // Now add denies.
        Set<String> denied = fDenied.get(capability);
        if (denied == null)
        {
            denied = new HashSet<String>();
            fDenied.put(capability, denied);
        }
        for (String authority : auths)
        {
            if (AuthorityType.getAuthorityType(authority) == AuthorityType.ADMIN)
            {
                continue;
            }
            denied.add(authority);
        }
    }

    /* (non-Javadoc)
     * @see org.alfresco.service.simple.permission.ACL#getAllowed(java.lang.String)
     */
    public Set<String> getAllowed(String capability)
    {
        capability = capability.toLowerCase();
        digest();
        Set<String> allowed = new HashSet<String>();
        allowed.add(AuthorityType.ADMIN.getFixedString());
        // Add the explicitly allowed.
        Set<String> expAllowed = fAllowed.get(capability);
        if (expAllowed == null)
        {
            return allowed;
        }
        allowed.addAll(expAllowed);
        for (String authority : expAllowed)
        {
            allowed.addAll(fCapabilityRegistry.getContainedAuthorities(authority));
        }
        // Now remove based on denials.
        Set<String> denied = fDenied.get(capability);
        if (denied == null)
        {
            return allowed;
        }
        allowed.removeAll(denied);
        // Now those that are indirectly denied.
        for (String authority : denied)
        {
            allowed.removeAll(fCapabilityRegistry.getContainedAuthorities(authority));
        }
        return allowed;
    }

    /* (non-Javadoc)
     * @see org.alfresco.service.simple.permission.ACL#getCapabilities(java.lang.String, boolean)
     */
    public Set<String> getCapabilities(String authority, boolean isOwner)
    {
        authority = fCapabilityRegistry.normalizeAuthority(authority);
        digest();
        AuthorityType type = AuthorityType.getAuthorityType(authority);
        if (type == AuthorityType.ADMIN)
        {
            return fCapabilityRegistry.getAllCapabilities();
        }
        Set<String> capabilities = new HashSet<String>();
        // First run through the allowed entries.
        Set<String> containers = null;
        for (Map.Entry<String, Set<String>> entry : fAllowed.entrySet())
        {
            if (entry.getValue().contains(authority))
            {
                capabilities.add(entry.getKey());
                continue;
            }
            if (containers == null)
            {
                containers = fCapabilityRegistry.getContainerAuthorities(authority);
            }
            for (String auth : containers)
            {
                if (entry.getValue().contains(auth))
                {
                    capabilities.add(entry.getKey());
                    break;
                }
            }
        }
        // Now go through the denials.
        for (Map.Entry<String, Set<String>> entry : fDenied.entrySet())
        {
            if (!capabilities.contains(entry.getKey()))
            {
                continue;
            }
            Set<String> denied = entry.getValue();
            if (denied.contains(authority))
            {
                capabilities.remove(entry.getKey());
                continue;
            }
            if (containers == null)
            {
                containers = fCapabilityRegistry.getContainerAuthorities(authority);
            }
            for (String auth : containers)
            {
                if (denied.contains(auth))
                {
                    capabilities.remove(entry.getKey());
                    break;
                }
            }
        }
        return capabilities;
    }

    /* (non-Javadoc)
     * @see org.alfresco.service.simple.permission.ACL#getStringRepresentation()
     */
    public String getStringRepresentation()
    {
        if (fStringRep != null)
        {
            return fStringRep;
        }
        StringBuilder builder = new StringBuilder();
        builder.append(fInherit ? 'i' : 'n');
        builder.append('|');
        int count = 0;
        for (Map.Entry<String, Set<String>> entry : fAllowed.entrySet())
        {
            builder.append(Integer.toString(fCapabilityRegistry.getCapabilityID(entry.getKey()), 32));
            for (String authority : entry.getValue())
            {
                builder.append(';');
                builder.append(Integer.toString(fCapabilityRegistry.getAuthorityID(authority), 32));
            }
            if (count++ < fAllowed.size() - 1)
            {
                builder.append(':');
            }
        }
        builder.append('|');
        count = 0;
        for (Map.Entry<String, Set<String>> entry : fDenied.entrySet())
        {
            builder.append(Integer.toString(fCapabilityRegistry.getCapabilityID(entry.getKey()), 32));
            for (String authority : entry.getValue())
            {
                builder.append(';');
                builder.append(Integer.toString(fCapabilityRegistry.getAuthorityID(authority), 32));
            }
            if (count++ < fDenied.size() - 1)
            {
                builder.append(':');
            }
        }
        return builder.toString();
    }

    /* (non-Javadoc)
     * @see org.alfresco.service.simple.permission.ACL#inherits()
     */
    public boolean inherits()
    {
        digest();
        return fInherit;
    }
}