/*
 * 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.shiro.subject;

import org.apache.shiro.util.CollectionUtils;

import java.util.*;

/**
 * Default implementation of the {@link PrincipalMap} interface.
 *
 * *EXPERIMENTAL for Shiro 1.2 - DO NOT USE YET*
 *
 * @since 1.2
 */
public class SimplePrincipalMap implements PrincipalMap {

    //Key: realm name, Value: map of principals specific to that realm
    //                        internal map - key: principal name, value: principal
    private Map<String, Map<String, Object>> realmPrincipals;

    //maintains the principals from all realms plus any that are modified via the Map modification methods
    //this ensures a fast lookup of any named principal instead of needing to iterate over
    //the realmPrincipals for each lookup.
    private Map<String, Object> combinedPrincipals;

    public SimplePrincipalMap() {
        this(null);
    }

    public SimplePrincipalMap(Map<String, Map<String, Object>> backingMap) {
        if (!CollectionUtils.isEmpty(backingMap)) {
            this.realmPrincipals = backingMap;
            for (Map<String, Object> principals : this.realmPrincipals.values()) {
                if (!CollectionUtils.isEmpty(principals) ) {
                    ensureCombinedPrincipals().putAll(principals);
                }
            }
        }
    }

    public int size() {
        return CollectionUtils.size(this.combinedPrincipals);
    }

    protected Map<String, Object> ensureCombinedPrincipals() {
        if (this.combinedPrincipals == null) {
            this.combinedPrincipals = new HashMap<String, Object>();
        }
        return this.combinedPrincipals;
    }

    public boolean containsKey(Object o) {
        return this.combinedPrincipals != null && this.combinedPrincipals.containsKey(o);
    }

    public boolean containsValue(Object o) {
        return this.combinedPrincipals != null && this.combinedPrincipals.containsKey(o);
    }

    public Object get(Object o) {
        return this.combinedPrincipals != null && this.combinedPrincipals.containsKey(o);
    }

    public Object put(String s, Object o) {
        return ensureCombinedPrincipals().put(s, o);
    }

    public Object remove(Object o) {
        return this.combinedPrincipals != null ? this.combinedPrincipals.remove(o) : null;
    }

    public void putAll(Map<? extends String, ?> map) {
        if (!CollectionUtils.isEmpty(map)) {
            ensureCombinedPrincipals().putAll(map);
        }
    }

    public Set<String> keySet() {
        return CollectionUtils.isEmpty(this.combinedPrincipals) ?
                Collections.<String>emptySet() :
                Collections.unmodifiableSet(this.combinedPrincipals.keySet());
    }

    public Collection<Object> values() {
        return CollectionUtils.isEmpty(this.combinedPrincipals) ?
                Collections.emptySet() :
                Collections.unmodifiableCollection(this.combinedPrincipals.values());
    }

    public Set<Entry<String, Object>> entrySet() {
        return CollectionUtils.isEmpty(this.combinedPrincipals) ?
                Collections.<Entry<String,Object>>emptySet() :
                Collections.unmodifiableSet(this.combinedPrincipals.entrySet());
    }

    public void clear() {
        this.realmPrincipals = null;
        this.combinedPrincipals = null;
    }

    public Object getPrimaryPrincipal() {
        //heuristic - just use the first one we come across:
        return !CollectionUtils.isEmpty(this.combinedPrincipals) ?
                this.combinedPrincipals.values().iterator().next() :
                null;
    }

    public <T> T oneByType(Class<T> type) {
        if (CollectionUtils.isEmpty(this.combinedPrincipals)) {
            return null;
        }
        for( Object value : this.combinedPrincipals.values()) {
            if (type.isInstance(value) ) {
                return type.cast(value);
            }
        }
        return null;
    }

    public <T> Collection<T> byType(Class<T> type) {
        if (CollectionUtils.isEmpty(this.combinedPrincipals)) {
            return Collections.emptySet();
        }
        Collection<T> instances = null;
        for( Object value : this.combinedPrincipals.values()) {
            if (type.isInstance(value) ) {
                if (instances == null) {
                    instances = new ArrayList<T>();
                }
                instances.add(type.cast(value));
            }
        }
        return instances != null ? instances : Collections.<T>emptyList();
    }

    public List asList() {
        if (CollectionUtils.isEmpty(this.combinedPrincipals)) {
            return Collections.emptyList();
        }
        List<Object> list = new ArrayList<Object>(this.combinedPrincipals.size());
        list.addAll(this.combinedPrincipals.values());
        return list;
    }

    public Set asSet() {
        if (CollectionUtils.isEmpty(this.combinedPrincipals)) {
            return Collections.emptySet();
        }
        Set<Object> set = new HashSet<Object>(this.combinedPrincipals.size());
        set.addAll(this.combinedPrincipals.values());
        return set;
    }

    public Collection fromRealm(String realmName) {
        if (CollectionUtils.isEmpty(this.realmPrincipals)) {
            return Collections.emptySet();
        }
        Map<String,Object> principals = this.realmPrincipals.get(realmName);
        if (CollectionUtils.isEmpty(principals)) {
            return Collections.emptySet();
        }
        return Collections.unmodifiableCollection(principals.values());
    }

    public Set<String> getRealmNames() {
        if (CollectionUtils.isEmpty(this.realmPrincipals)) {
            return Collections.emptySet();
        }
        return Collections.unmodifiableSet(this.realmPrincipals.keySet());
    }

    public boolean isEmpty() {
        return CollectionUtils.isEmpty(this.combinedPrincipals);
    }

    public Iterator iterator() {
        return asList().iterator();
    }

    public Map<String, Object> getRealmPrincipals(String name) {
        if (this.realmPrincipals == null) {
            return null;
        }
        Map<String,Object> principals = this.realmPrincipals.get(name);
        if (principals == null) {
            return null;
        }
        return Collections.unmodifiableMap(principals);
    }

    public Map<String,Object> setRealmPrincipals(String realmName, Map<String, Object> principals) {
        if (realmName == null) {
            throw new NullPointerException("realmName argument cannot be null.");
        }
        if (this.realmPrincipals == null) {
            if (!CollectionUtils.isEmpty(principals)) {
                this.realmPrincipals = new HashMap<String,Map<String,Object>>();
                return this.realmPrincipals.put(realmName, new HashMap<String,Object>(principals));
            } else {
                return null;
            }
        } else {
            Map<String,Object> existingPrincipals = this.realmPrincipals.remove(realmName);
            if (!CollectionUtils.isEmpty(principals)) {
                this.realmPrincipals.put(realmName, new HashMap<String,Object>(principals));
            }
            return existingPrincipals;
        }
    }

    public Object setRealmPrincipal(String realmName, String principalName, Object principal) {
        if (realmName == null) {
            throw new NullPointerException("realmName argument cannot be null.");
        }
        if (principalName == null) {
            throw new NullPointerException(("principalName argument cannot be null."));
        }
        if (principal == null) {
            return removeRealmPrincipal(realmName, principalName);
        }
        if (this.realmPrincipals == null) {
            this.realmPrincipals = new HashMap<String,Map<String,Object>>();
        }
        Map<String,Object> principals = this.realmPrincipals.get(realmName);
        if (principals == null) {
            principals = new HashMap<String,Object>();
            this.realmPrincipals.put(realmName, principals);
        }
        return principals.put(principalName, principal);
    }

    public Object getRealmPrincipal(String realmName, String principalName) {
        if (realmName == null) {
            throw new NullPointerException("realmName argument cannot be null.");
        }
        if (principalName == null) {
            throw new NullPointerException(("principalName argument cannot be null."));
        }
        if (this.realmPrincipals == null) {
            return null;
        }
        Map<String,Object> principals = this.realmPrincipals.get(realmName);
        if (principals != null) {
            return principals.get(principalName);
        }
        return null;
    }

    public Object removeRealmPrincipal(String realmName, String principalName) {
        if (realmName == null) {
            throw new NullPointerException("realmName argument cannot be null.");
        }
        if (principalName == null) {
            throw new NullPointerException(("principalName argument cannot be null."));
        }
        if (this.realmPrincipals == null) {
            return null;
        }
        Map<String,Object> principals = this.realmPrincipals.get(realmName);
        if (principals != null) {
            return principals.remove(principalName);
        }
        return null;
    }
}
