/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * 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 the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>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.apereo.portal.persondir;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.jasig.services.persondir.IPersonAttributes;
import org.jasig.services.persondir.support.AbstractDefaultAttributePersonAttributeDao;
import org.jasig.services.persondir.support.AttributeNamedPersonImpl;
import org.jasig.services.persondir.support.IUsernameAttributeProvider;
import org.jasig.services.persondir.support.MultivaluedPersonAttributeUtils;
import org.jasig.services.persondir.support.NamedPersonImpl;

/**
 * LocalAccountPersonAttributeDao provides a person directory implementation that uses uPortal's
 * internal account store. This implementation overrides several methods of
 * AbstractQueryPersonAttributeDao to allow for the use of arbitrary user attributes without
 * requiring an administrator to add new entries to the result or query attribute mappings.
 */
public class LocalAccountPersonAttributeDao extends AbstractDefaultAttributePersonAttributeDao {

    private ILocalAccountDao localAccountDao;
    private Map<String, Set<String>> resultAttributeMapping;
    private Set<String> possibleUserAttributes;
    private String unmappedUsernameAttribute = null;
    private String displayNameAttribute = "displayName";
    private Map<String, Set<String>> queryAttributeMapping;

    /**
     * Set the local portal account DAO.
     *
     * @param localAccountDao
     */
    public void setLocalAccountDao(ILocalAccountDao localAccountDao) {
        this.localAccountDao = localAccountDao;
    }

    public void setQueryAttributeMapping(final Map<String, ?> queryAttributeMapping) {
        final Map<String, Set<String>> parsedQueryAttributeMapping =
                MultivaluedPersonAttributeUtils.parseAttributeToAttributeMapping(
                        queryAttributeMapping);

        if (parsedQueryAttributeMapping.containsKey("")) {
            throw new IllegalArgumentException(
                    "The map from attribute names to attributes must not have any empty keys.");
        }

        this.queryAttributeMapping = parsedQueryAttributeMapping;
    }

    /** @return the resultAttributeMapping */
    public Map<String, Set<String>> getResultAttributeMapping() {
        return resultAttributeMapping;
    }
    /**
     * Set the {@link Map} to use for mapping from a data layer name to an attribute name or {@link
     * Set} of attribute names. Data layer names that are specified but have null mappings will use
     * the column name for the attribute name. Data layer names that are not specified as keys in
     * this {@link Map} will be ignored. <br>
     * The passed {@link Map} must have keys of type {@link String} and values of type {@link
     * String} or a {@link Set} of {@link String}.
     *
     * @param resultAttributeMapping {@link Map} from column names to attribute names, may not be
     *     null.
     * @throws IllegalArgumentException If the {@link Map} doesn't follow the rules stated above.
     * @see MultivaluedPersonAttributeUtils#parseAttributeToAttributeMapping(Map)
     */
    public void setResultAttributeMapping(Map<String, ?> resultAttributeMapping) {
        final Map<String, Set<String>> parsedResultAttributeMapping =
                MultivaluedPersonAttributeUtils.parseAttributeToAttributeMapping(
                        resultAttributeMapping);

        if (parsedResultAttributeMapping.containsKey("")) {
            throw new IllegalArgumentException(
                    "The map from attribute names to attributes must not have any empty keys.");
        }

        final Collection<String> userAttributes =
                MultivaluedPersonAttributeUtils.flattenCollection(
                        parsedResultAttributeMapping.values());

        this.resultAttributeMapping = parsedResultAttributeMapping;
        this.possibleUserAttributes =
                Collections.unmodifiableSet(new LinkedHashSet<String>(userAttributes));
    }

    /** @return the userNameAttribute */
    public String getUnmappedUsernameAttribute() {
        return unmappedUsernameAttribute;
    }
    /**
     * The returned attribute to use as the userName for the mapped IPersons. If null the {@link
     * #setDefaultAttributeName(String)} value will be used and if that is null the {@link
     * AttributeNamedPersonImpl#DEFAULT_USER_NAME_ATTRIBUTE} value is used.
     *
     * @param userNameAttribute the userNameAttribute to set
     */
    public void setUnmappedUsernameAttribute(String userNameAttribute) {
        this.unmappedUsernameAttribute = userNameAttribute;
    }

    /**
     * Return the list of all possible attribute names. This implementation queries the database to
     * provide a list of all mapped attribute names, plus all attribute keys currently in-use in the
     * database.
     *
     * @return Set
     */
    @Override
    public Set<String> getPossibleUserAttributeNames() {
        final Set<String> names = new HashSet<String>();
        names.addAll(this.possibleUserAttributes);
        names.addAll(localAccountDao.getCurrentAttributeNames());
        names.add(displayNameAttribute);
        return names;
    }

    /**
     * Return the list of all possible query attributes. This implementation queries the database to
     * provide a list of all mapped query attribute names, plus all attribute keys currently in-use
     * in the database.
     *
     * @return Set
     */
    @Override
    public Set<String> getAvailableQueryAttributes() {
        if (this.queryAttributeMapping == null) {
            return Collections.emptySet();
        }

        return Collections.unmodifiableSet(this.queryAttributeMapping.keySet());
    }

    /* (non-Javadoc)
     * @see org.jasig.services.persondir.IPersonAttributeDao#getPeopleWithMultivaluedAttributes(java.util.Map)
     */
    @Override
    public final Set<IPersonAttributes> getPeopleWithMultivaluedAttributes(
            Map<String, List<Object>> query) {
        Validate.notNull(query, "query may not be null.");

        //Generate the query to pass to the subclass
        final LocalAccountQuery queryBuilder = this.generateQuery(query);
        if (queryBuilder == null) {
            this.logger.debug(
                    "No queryBuilder was generated for query " + query + ", null will be returned");

            return null;
        }

        //Get the username from the query, if specified
        final IUsernameAttributeProvider usernameAttributeProvider =
                this.getUsernameAttributeProvider();

        //Execute the query in the subclass
        final List<ILocalAccountPerson> unmappedPeople = localAccountDao.getPeople(queryBuilder);
        if (unmappedPeople == null) {
            return null;
        }

        //Map the attributes of the found people according to resultAttributeMapping if it is set
        final Set<IPersonAttributes> mappedPeople = new LinkedHashSet<IPersonAttributes>();
        for (final ILocalAccountPerson unmappedPerson : unmappedPeople) {
            final IPersonAttributes mappedPerson = this.mapPersonAttributes(unmappedPerson);
            mappedPeople.add(mappedPerson);
        }

        return Collections.unmodifiableSet(mappedPeople);
    }

    protected LocalAccountQuery generateQuery(Map<String, List<Object>> query) {
        LocalAccountQuery queryBuilder = new LocalAccountQuery();

        String userNameAttribute = this.getConfiguredUserNameAttribute();

        for (final Map.Entry<String, List<Object>> queryEntry : query.entrySet()) {

            String attrName = queryEntry.getKey();

            if (userNameAttribute.equals(attrName)) {
                String value = queryEntry.getValue().get(0).toString();
                queryBuilder.setUserName(value);
            } else {
                List<String> values = new ArrayList<String>();
                for (Object o : queryEntry.getValue()) {
                    values.add(o.toString());
                }
                queryBuilder.setAttribute(attrName, values);
            }
        }

        if (this.logger.isDebugEnabled()) {
            this.logger.debug(
                    "Generated query builder '" + queryBuilder + "' from query Map " + query + ".");
        }

        return queryBuilder;
    }

    /**
     * This implementation uses the result attribute mapping to supplement, rather than replace, the
     * attributes returned from the database.
     */
    protected IPersonAttributes mapPersonAttributes(final ILocalAccountPerson person) {

        final Map<String, List<Object>> mappedAttributes =
                new LinkedHashMap<String, List<Object>>();
        mappedAttributes.putAll(person.getAttributes());

        // map the user's username to the portal's username attribute in the
        // attribute map
        mappedAttributes.put(
                this.getUsernameAttributeProvider().getUsernameAttribute(),
                Collections.<Object>singletonList(person.getName()));

        // if the user does not have a display name attribute set, attempt
        // to build one from the first and last name attributes
        if (!mappedAttributes.containsKey(displayNameAttribute)
                || mappedAttributes.get(displayNameAttribute).size() == 0
                || StringUtils.isBlank(
                        (String) mappedAttributes.get(displayNameAttribute).get(0))) {
            final List<Object> firstNames = mappedAttributes.get("givenName");
            final List<Object> lastNames = mappedAttributes.get("sn");
            final StringBuilder displayName = new StringBuilder();
            if (firstNames != null && firstNames.size() > 0) {
                displayName.append(firstNames.get(0)).append(" ");
            }
            if (lastNames != null && lastNames.size() > 0) {
                displayName.append(lastNames.get(0));
            }
            mappedAttributes.put(
                    displayNameAttribute,
                    Collections.<Object>singletonList(displayName.toString()));
        }

        for (final Map.Entry<String, Set<String>> resultAttrEntry :
                this.getResultAttributeMapping().entrySet()) {
            final String dataKey = resultAttrEntry.getKey();

            //Only map found data attributes
            if (mappedAttributes.containsKey(dataKey)) {
                Set<String> resultKeys = resultAttrEntry.getValue();

                //If dataKey has no mapped resultKeys just use the dataKey
                if (resultKeys == null) {
                    resultKeys = Collections.singleton(dataKey);
                }

                //Add the value to the mapped attributes for each mapped key
                final List<Object> value = mappedAttributes.get(dataKey);
                for (final String resultKey : resultKeys) {
                    if (resultKey == null) {
                        //TODO is this possible?
                        if (!mappedAttributes.containsKey(dataKey)) {
                            mappedAttributes.put(dataKey, value);
                        }
                    } else if (!mappedAttributes.containsKey(resultKey)) {
                        mappedAttributes.put(resultKey, value);
                    }
                }
            }
        }

        final IPersonAttributes newPerson;

        final String name = person.getName();
        if (name != null) {
            newPerson = new NamedPersonImpl(name, mappedAttributes);
        } else {
            final String userNameAttribute = this.getConfiguredUserNameAttribute();
            newPerson = new AttributeNamedPersonImpl(userNameAttribute, mappedAttributes);
        }

        return newPerson;
    }

    /**
     * @return The appropriate attribute to user for the user name. Since {@link
     *     #getDefaultAttributeName()} should never return null this method should never return null
     *     either.
     */
    protected String getConfiguredUserNameAttribute() {
        //If configured explicitly use it
        if (this.unmappedUsernameAttribute != null) {
            return this.unmappedUsernameAttribute;
        }

        final IUsernameAttributeProvider usernameAttributeProvider =
                this.getUsernameAttributeProvider();
        return usernameAttributeProvider.getUsernameAttribute();
    }
}
