/*
 *  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.vysper.xmpp.modules.roster;

import static org.apache.vysper.compliance.SpecCompliant.ComplianceCoverage.COMPLETE;
import static org.apache.vysper.compliance.SpecCompliant.ComplianceCoverage.PARTIAL;
import static org.apache.vysper.compliance.SpecCompliant.ComplianceStatus.FINISHED;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.vysper.compliance.SpecCompliance;
import org.apache.vysper.compliance.SpecCompliant;
import org.apache.vysper.xml.fragment.Attribute;
import org.apache.vysper.xml.fragment.XMLElement;
import org.apache.vysper.xml.fragment.XMLElementVerifier;
import org.apache.vysper.xml.fragment.XMLSemanticError;
import org.apache.vysper.xmpp.addressing.Entity;
import org.apache.vysper.xmpp.addressing.EntityFormatException;
import org.apache.vysper.xmpp.addressing.EntityImpl;
import org.apache.vysper.xmpp.modules.roster.persistence.RosterManager;
import org.apache.vysper.xmpp.stanza.IQStanza;

/**
 * some roster logic
 * @author The Apache MINA Project (dev@mina.apache.org)
 */
public class RosterUtils {

    /**
     * takes the roster of a user and groups items by subscription state. this is helpful when all FROM items
     * are needed and then all TO items - but the roster is only iterated once. 
     */
    public static Map<SubscriptionType, List<RosterItem>> getRosterItemsByState(RosterManager rosterManager, Entity user) {
        Map<SubscriptionType, List<RosterItem>> rosterItemMap = new HashMap<SubscriptionType, List<RosterItem>>();

        rosterItemMap.put(SubscriptionType.FROM, new ArrayList<RosterItem>());
        rosterItemMap.put(SubscriptionType.TO, new ArrayList<RosterItem>());
        rosterItemMap.put(SubscriptionType.BOTH, new ArrayList<RosterItem>());
        rosterItemMap.put(SubscriptionType.REMOVE, new ArrayList<RosterItem>());
        rosterItemMap.put(SubscriptionType.NONE, new ArrayList<RosterItem>());

        Roster roster;
        try {
            roster = rosterManager.retrieve(user);
        } catch (RosterException e) {
            // TODO: make this errorhandling more intelligent
            throw new RuntimeException("could not retrieve roster for user " + user.getFullQualifiedName());
        }

        // get items sorted by subscription type
        for (RosterItem rosterItem : roster) {
            rosterItemMap.get(rosterItem.getSubscriptionType()).add(rosterItem);
        }

        return rosterItemMap;
    }

    /**
     * extracts a roster item from the given stanza
     */
    public static RosterItem parseRosterItem(IQStanza stanza) throws RosterBadRequestException,
            RosterNotAcceptableException {
        return parseRosterItem(stanza, false); // do not read subscription types (except 'remove')
    }

    /**
     * extracts a roster item from the given stanza, with relaxed semantical checks for testing
     */
    public static RosterItem parseRosterItemForTesting(IQStanza stanza) throws RosterBadRequestException,
            RosterNotAcceptableException {
        return parseRosterItem(stanza, true); // do also parse subscription types
    }

    /**
     * extracts a roster item from the stanza and checks for integrity according to the XMPP spec
     */
    @SpecCompliance(compliant = {
            @SpecCompliant(spec = "rfc3921bis-08", section = "2.1.1", status = FINISHED, coverage = COMPLETE),
            @SpecCompliant(spec = "rfc3921bis-08", section = "2.1.3", status = FINISHED, coverage = PARTIAL, comment = "handles the conformance rules 1-3 when parseSubscriptionTypes is set to false") })
    private static RosterItem parseRosterItem(IQStanza stanza, boolean parseSubscriptionTypes)
            throws RosterBadRequestException, RosterNotAcceptableException {
        XMLElement queryElement;
        try {
            queryElement = stanza.getSingleInnerElementsNamed("query");
            if (queryElement == null)
                throw new XMLSemanticError("missing query node");
        } catch (XMLSemanticError xmlSemanticError) {
            throw new RosterBadRequestException("roster set needs a single query node.");
        }

        XMLElement itemElement;
        try {
            itemElement = queryElement.getSingleInnerElementsNamed("item");
            if (itemElement == null)
                throw new XMLSemanticError("missing item node");
        } catch (XMLSemanticError xmlSemanticError) {
            throw new RosterBadRequestException("roster set needs a single item node.");
        }

        Attribute attributeJID = itemElement.getAttribute("jid");
        if (attributeJID == null || attributeJID.getValue() == null)
            throw new RosterBadRequestException("missing 'jid' attribute on item node");

        XMLElementVerifier verifier = itemElement.getVerifier();
        String name = verifier.attributePresent("name") ? itemElement.getAttribute("name").getValue() : null;
        if (name != null && name.length() > RosterConfiguration.ROSTER_ITEM_NAME_MAX_LENGTH) {
            throw new RosterNotAcceptableException("roster name too long: " + name.length());
        }

        SubscriptionType subscription = verifier.attributePresent("subscription") ? SubscriptionType
                .valueOf(itemElement.getAttribute("subscription").getValue().toUpperCase()) : SubscriptionType.NONE;
        if (!parseSubscriptionTypes && subscription != SubscriptionType.REMOVE)
            subscription = SubscriptionType.NONE; // roster remove is always tolerated

        AskSubscriptionType askSubscriptionType = AskSubscriptionType.NOT_SET;
        if (parseSubscriptionTypes) {
            askSubscriptionType = verifier.attributePresent("ask") ? AskSubscriptionType.valueOf("ASK_"
                    + itemElement.getAttribute("ask").getValue().toUpperCase()) : AskSubscriptionType.NOT_SET;
        }

        String contactJid = attributeJID.getValue();
        Entity contact;
        try {
            contact = EntityImpl.parse(contactJid);
        } catch (EntityFormatException e) {
            throw new RosterNotAcceptableException("jid cannot be parsed: " + contactJid);
        }

        List<RosterGroup> groups = new ArrayList<RosterGroup>();
        List<XMLElement> groupElements = itemElement.getInnerElementsNamed("group");
        if (groupElements != null) {
            for (XMLElement groupElement : groupElements) {
                String groupName = null;
                try {
                    groupName = groupElement.getSingleInnerText().getText();
                } catch (XMLSemanticError xmlSemanticError) {
                    throw new RosterBadRequestException("roster item group node is malformed");
                }
                if (StringUtils.isEmpty(groupName)) {
                    throw new RosterNotAcceptableException("roster item group name of zero length");
                } else if (groupName.length() > RosterConfiguration.ROSTER_GROUP_NAME_MAX_LENGTH) {
                    throw new RosterNotAcceptableException("roster item group name too long: " + groupName.length());
                }
                RosterGroup group = new RosterGroup(groupName);
                if (groups.contains(group) && !RosterConfiguration.ROSTER_ITEM_GROUP_ALLOW_DUPLICATES) {
                    throw new RosterNotAcceptableException("duplicate roster group name: " + groupName);
                } else {
                    groups.add(group);
                }
            }
        }

        RosterItem rosterItem = new RosterItem(contact, name, subscription, askSubscriptionType, groups);
        return rosterItem;
    }
}
