/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.acl;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.jcr.NamespaceException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;

import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry;
import org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.AccessControlConstants;
import org.apache.jackrabbit.jcr2spi.state.NodeState;
import org.apache.jackrabbit.jcr2spi.state.PropertyState;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.QValue;
import org.apache.jackrabbit.spi.QValueFactory;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.value.ValueFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class AccessControlListImpl implements JackrabbitAccessControlList, AccessControlConstants {

    private static final Logger log = LoggerFactory.getLogger(AccessControlListImpl.class);

    private final String jcrPath;

    private final List<AccessControlEntry> entries;
    private final QValueFactory qValueFactory;
    private final NamePathResolver resolver;

    /**
     * Namespace sensitive name of the REP_GLOB property in standard JCR form.
     */
    private final String jcrRepGlob;

    public AccessControlListImpl(String jcrPath, NamePathResolver resolver, QValueFactory qValueFactory) throws RepositoryException {
        this.jcrPath = jcrPath;
        this.entries = new ArrayList<AccessControlEntry>();
        this.qValueFactory = qValueFactory;
        this.resolver = resolver;

        try {
            jcrRepGlob = resolver.getJCRName(P_GLOB);
        } catch (NamespaceException e) {
            throw new RepositoryException(e.getMessage());
        }
    }

    AccessControlListImpl(NodeState aclNode, String aclPath, NamePathResolver resolver, QValueFactory factory, AccessControlManager acm) throws RepositoryException {
        this(aclPath, resolver, factory);

        NodeEntry entry = (NodeEntry) aclNode.getHierarchyEntry();
        Iterator<NodeEntry> it = entry.getNodeEntries();

        while(it.hasNext()) {
            NodeState aceNode = it.next().getNodeState();
            try {

                PropertyState ps = aceNode.getPropertyState(N_REP_PRINCIPAL_NAME);

                // rep:principal property
                String principalName = ps.getValue().getString();
                Principal principal = createPrincipal(principalName);

                // rep:privileges property
                ps = aceNode.getPropertyState(N_REP_PRIVILEGES);

                QValue[] values = ps.getValues();
                Privilege[] privileges = new Privilege[values.length];
                for (int i = 0; i < values.length; i++) {
                    privileges[i] = acm.privilegeFromName(values[i].getString());
                }

                // rep:glob property -> restrictions
                Map<Name, QValue> restrictions = null;
                if (aceNode.hasPropertyName(P_GLOB)) {
                    ps = aceNode.getPropertyState(P_GLOB);
                    restrictions = Collections.singletonMap(ps.getName(), ps.getValue());
                }

                // the isAllow flag
                boolean isAllow = NT_REP_GRANT_ACE.equals(aceNode.getNodeTypeName());
                // build the entry
                AccessControlEntry ace = new AccessControlEntryImpl(principal, privileges, isAllow, restrictions, Collections.EMPTY_MAP, resolver, qValueFactory);
                entries.add(ace);
            } catch (RepositoryException e) {
                log.debug("Fail to create Entry for "+ aceNode.getName().toString());
            }
        }
    }

    //--------------------------------------------------< AccessControlList >---
    @Override
    public AccessControlEntry[] getAccessControlEntries()
            throws RepositoryException {
        return entries.toArray(new AccessControlEntry[entries.size()]);
    }

    @Override
    public boolean addAccessControlEntry(Principal principal, Privilege[] privileges) throws AccessControlException, RepositoryException {
        return addEntry(principal, privileges, true, Collections.<String, Value>emptyMap());
    }

    @Override
    public void removeAccessControlEntry(AccessControlEntry ace)
            throws AccessControlException, RepositoryException {
        if (entries.contains(ace)) {
            entries.remove(ace);
        } else {
            throw new AccessControlException("Entry not present in this list");
        }
    }

    //----------------------------------------< JackrabbitAccessControlList >---
    @Override
    public String getPath() {
        return jcrPath;
    }

    @Override
    public boolean isEmpty() {
        return entries.isEmpty();
    }

    @Override
    public int size() {
        return entries.size();
    }

    @Override
    public boolean addEntry(Principal principal, Privilege[] privileges,
                            boolean isAllow) throws AccessControlException, RepositoryException {
        return addEntry(principal, privileges, isAllow, Collections.<String, Value>emptyMap());
    }

    @Override
    public boolean addEntry(Principal principal, Privilege[] privileges,
                            boolean isAllow, Map<String, Value> restrictions,
                            Map<String, Value[]> mvRestrictions) throws AccessControlException,
            RepositoryException {


        // create entry to be added
        Map<Name, QValue> rs = createRestrictions(restrictions);
        Map<Name, Iterable<QValue>> mvRs = createMvRestrictions(mvRestrictions);
        AccessControlEntry entry = createEntry(principal, privileges, isAllow, rs, mvRs);

        return entries.add(entry);

    }

    @Override
    public boolean addEntry(Principal principal, Privilege[] privileges,
                            boolean isAllow, Map<String, Value> restrictions)
            throws AccessControlException, RepositoryException {
        return addEntry(principal, privileges, isAllow, restrictions, Collections.EMPTY_MAP);
    }

    @Override
    public String[] getRestrictionNames() throws RepositoryException {
        return new String[] {jcrRepGlob};
    }

    @Override
    public int getRestrictionType(String restrictionName) throws RepositoryException {
        if (!jcrRepGlob.equals(restrictionName)) {
            // JR2 feature
            return PropertyType.UNDEFINED;
        }
        return PropertyType.STRING;
    }

    @Override
    public void orderBefore(AccessControlEntry srcEntry,
                            AccessControlEntry destEntry) throws AccessControlException,
            UnsupportedRepositoryOperationException, RepositoryException {
        // TODO
        throw new UnsupportedRepositoryOperationException("not yet implemented");
    }

    //-------------------------------------------------------------< Object >---
    /**
     * Returns zero to satisfy the Object equals/hashCode contract.
     * This class is mutable and not meant to be used as a hash key.
     *
     * @return always zero
     * @see Object#hashCode()
     */
    @Override
    public int hashCode() {
        return 0;
    }

    /**
     * Returns true if the path and the entries are equal; false otherwise.
     *
     * @param obj Object to be tested.
     * @return true if the path and the entries are equal; false otherwise.
     * @see Object#equals(Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }

        if (obj instanceof AccessControlListImpl) {
            AccessControlListImpl acl = (AccessControlListImpl) obj;
            return jcrPath.equals(acl.jcrPath) && entries.equals(acl.entries);
        }
        return false;
    }

    //------------------------------------------------------------< private >---
    private AccessControlEntry createEntry(Principal principal, Privilege[] privileges, boolean isAllow,
                                           Map<Name, QValue> restrictions, Map<Name, Iterable<QValue>> mvRestrictions) throws RepositoryException {
        return new AccessControlEntryImpl(principal, privileges, isAllow, restrictions, mvRestrictions, resolver, qValueFactory);
    }

    private Map<Name, QValue> createRestrictions(Map<String, Value> restrictions) throws RepositoryException {
        Map<Name, QValue> rs = new HashMap<Name, QValue>(restrictions.size());
        for (String restName : restrictions.keySet()) {
            Value v = restrictions.get(restName);
            rs.put(resolver.getQName(restName), ValueFormat.getQValue(v, resolver, qValueFactory));
        }
        return rs;
    }

    private Map<Name, Iterable<QValue>> createMvRestrictions(Map<String, Value[]> restrictions) throws RepositoryException {
            Map<Name, Iterable<QValue>> rs = new HashMap<Name, Iterable<QValue>>(restrictions.size());
            for (String restName : restrictions.keySet()) {
                QValue[] qvs = ValueFormat.getQValues(restrictions.get(restName), resolver, qValueFactory);
                rs.put(resolver.getQName(restName), Arrays.asList(qvs));
            }
            return rs;
        }

    private static Principal createPrincipal(final String name) {
        return new Principal() {
            @Override
            public String getName() {
                return name;
            }
        };
    }
}
