/*
 *  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.core.im.handler;

import static org.apache.vysper.compliance.SpecCompliant.ComplianceStatus.IN_PROGRESS;
import static org.apache.vysper.compliance.SpecCompliant.ComplianceStatus.NOT_STARTED;
import static org.apache.vysper.xmpp.modules.roster.AskSubscriptionType.ASK_SUBSCRIBE;
import static org.apache.vysper.xmpp.modules.roster.AskSubscriptionType.ASK_SUBSCRIBED;
import static org.apache.vysper.xmpp.modules.roster.RosterSubscriptionMutator.Result.ALREADY_SET;
import static org.apache.vysper.xmpp.modules.roster.RosterSubscriptionMutator.Result.OK;
import static org.apache.vysper.xmpp.modules.roster.SubscriptionType.FROM;
import static org.apache.vysper.xmpp.modules.roster.SubscriptionType.NONE;
import static org.apache.vysper.xmpp.modules.roster.SubscriptionType.TO;
import static org.apache.vysper.xmpp.stanza.PresenceStanzaType.SUBSCRIBED;
import static org.apache.vysper.xmpp.stanza.PresenceStanzaType.isSubscriptionType;

import java.util.List;

import org.apache.vysper.compliance.SpecCompliance;
import org.apache.vysper.compliance.SpecCompliant;
import org.apache.vysper.xmpp.addressing.Entity;
import org.apache.vysper.xmpp.addressing.EntityImpl;
import org.apache.vysper.xmpp.delivery.LocalDeliveryUtils;
import org.apache.vysper.xmpp.delivery.StanzaRelay;
import org.apache.vysper.xmpp.delivery.failure.DeliveryException;
import org.apache.vysper.xmpp.delivery.failure.IgnoreFailureStrategy;
import org.apache.vysper.xmpp.modules.roster.RosterException;
import org.apache.vysper.xmpp.modules.roster.RosterItem;
import org.apache.vysper.xmpp.modules.roster.RosterStanzaUtils;
import org.apache.vysper.xmpp.modules.roster.RosterSubscriptionMutator;
import org.apache.vysper.xmpp.modules.roster.persistence.RosterManager;
import org.apache.vysper.xmpp.server.ServerRuntimeContext;
import org.apache.vysper.xmpp.server.SessionContext;
import org.apache.vysper.xmpp.stanza.PresenceStanza;
import org.apache.vysper.xmpp.stanza.PresenceStanzaType;
import org.apache.vysper.xmpp.stanza.Stanza;
import org.apache.vysper.xmpp.stanza.XMPPCoreStanzaVerifier;
import org.apache.vysper.xmpp.state.resourcebinding.ResourceRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * handling presence stanzas of type subscription
 *
 * TODO: review all the printStackTraces and throws and turn them into logs or stanza errors
 *
 * @author The Apache MINA Project (dev@mina.apache.org)
 */
public class PresenceSubscriptionHandler extends AbstractPresenceSpecializedHandler {

    final Logger logger = LoggerFactory.getLogger(PresenceSubscriptionHandler.class);

    @Override
    /*package*/Stanza executeCorePresence(ServerRuntimeContext serverRuntimeContext, boolean isOutboundStanza,
            SessionContext sessionContext, PresenceStanza presenceStanza, RosterManager rosterManager) {

        if (!isSubscriptionType(presenceStanza.getPresenceType())) {
            throw new RuntimeException("case not handled in availability handler"
                    + presenceStanza.getPresenceType().value());
        }

        // TODO: either use the resource associated with the session
        // (initiatingEntity)
        // or in case of multiple resources, use the from attribute or return an
        // error if the from attribute is not present.
        Entity initiatingEntity = sessionContext == null ? null : sessionContext.getInitiatingEntity();
        XMPPCoreStanzaVerifier verifier = presenceStanza.getCoreVerifier();
        ResourceRegistry registry = serverRuntimeContext.getResourceRegistry();

        PresenceStanzaType type = presenceStanza.getPresenceType();

        if (isOutboundStanza) {
            // this is an outbound subscription
            // request/approval/cancellation/unsubscription
            // stamp it with the bare JID of the user
            Entity user = initiatingEntity;
            PresenceStanza stampedStanza = buildPresenceStanza(user.getBareJID(), presenceStanza.getTo().getBareJID(),
                    presenceStanza.getPresenceType(), null);

            switch (type) {

            case SUBSCRIBE:
                // RFC3921bis-04#3.1.2
                // user requests subsription to contact
                handleOutboundSubscriptionRequest(stampedStanza, serverRuntimeContext, sessionContext, registry,
                        rosterManager);
                break;

            case SUBSCRIBED:
                // RFC3921bis-04#3.1.5
                // user approves subscription to requesting contact
                handleOutboundSubscriptionApproval(stampedStanza, serverRuntimeContext, sessionContext, registry,
                        rosterManager);
                break;

            case UNSUBSCRIBE:
                // RFC3921bis-04#3.3.2
                // user removes subscription from contact
                handleOutboundUnsubscription(stampedStanza, serverRuntimeContext, sessionContext, registry,
                        rosterManager);
                break;

            case UNSUBSCRIBED:
                // RFC3921bis-04#3.2.2
                // user approves unsubscription of contact
                handleOutboundSubscriptionCancellation(stampedStanza, serverRuntimeContext, sessionContext, registry,
                        rosterManager);
                break;

            default:
                throw new RuntimeException("unhandled case " + type.value());
            }

        } else /* inbound */{

            switch (type) {

            case SUBSCRIBE:
                // RFC3921bis-04#3.1.3
                // contact requests subscription to user
                return handleInboundSubscriptionRequest(presenceStanza, serverRuntimeContext, sessionContext, registry,
                        rosterManager);

            case SUBSCRIBED:
                // RFC3921bis-04#3.1.6
                // contact approves user's subsription request
                return handleInboundSubscriptionApproval(presenceStanza, serverRuntimeContext, sessionContext,
                        registry, rosterManager);

            case UNSUBSCRIBE:
                // RFC3921bis-04#3.3.3
                // contact unsubscribes
                handleInboundUnsubscription(presenceStanza, serverRuntimeContext, sessionContext, registry,
                        rosterManager);
                return null;

            case UNSUBSCRIBED:
                // RFC3921bis-04#3.2.3
                // contact denies subsription
                handleInboundSubscriptionCancellation(presenceStanza, serverRuntimeContext, sessionContext, registry,
                        rosterManager);
                return null;

            default:
                throw new RuntimeException("unhandled case " + type.value());

            }
        }
        return null;
    }

    @SpecCompliance(compliant = {
            @SpecCompliant(spec = "RFC3921bis-05", section = "3.3.3", status = IN_PROGRESS, comment = "current impl based hereupon"),
            @SpecCompliant(spec = "RFC3921bis-08", section = "3.3.3", status = NOT_STARTED, comment = "substantial additions from bis-05 not yet taken into account") })
    protected void handleInboundUnsubscription(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext,
            SessionContext sessionContext, ResourceRegistry registry, RosterManager rosterManager) {
        Entity contact = stanza.getFrom();
        Entity user = stanza.getTo();

        Entity userBareJid = user.getBareJID();
        Entity contactBareJid = contact.getBareJID();

        RosterItem rosterItem;
        try {
            rosterItem = rosterManager.getContact(userBareJid, contactBareJid);
        } catch (RosterException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        if (rosterItem == null)
            return;

        RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().remove(rosterItem, FROM);

        if (result != OK) {
            // TODO
            return;
        }

        // send roster push to all interested resources
        // TODO do this only once, since inbound is multiplexed on DeliveringInboundStanzaRelay level already
        List<String> resources = registry.getInterestedResources(user);
        for (String resource : resources) {
            Entity userResource = new EntityImpl(user, resource);
            Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource, sessionContext.nextSequenceValue(),
                    rosterItem);
            LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push);
        }

    }

    @SpecCompliance(compliant = {
            @SpecCompliant(spec = "RFC3921bis-05", section = "3.3.2", status = IN_PROGRESS, comment = "current impl based hereupon"),
            @SpecCompliant(spec = "RFC3921bis-08", section = "3.3.2", status = NOT_STARTED, comment = "rephrasing from bis-05 not yet taken into account") })
    protected void handleOutboundUnsubscription(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext,
            SessionContext sessionContext, ResourceRegistry registry, RosterManager rosterManager) {
        Entity user = stanza.getFrom();
        Entity contact = stanza.getTo();

        Entity userBareJid = user.getBareJID();
        Entity contactBareJid = contact.getBareJID();

        relayStanza(contact, stanza, sessionContext);

        RosterItem rosterItem = null;
        try {
            rosterItem = rosterManager.getContact(userBareJid, contactBareJid);
        } catch (RosterException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        if (rosterItem == null)
            return;

        RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().remove(rosterItem, TO);

        if (result != OK) {
            // TODO
            return;
        }

        relayStanza(contact, stanza, sessionContext);

        sendRosterUpdate(sessionContext, registry, user, rosterItem);
    }

    /**
     * send roster push to all of the user's interested resources
     */
    protected void sendRosterUpdate(SessionContext sessionContext, ResourceRegistry registry, Entity user,
            RosterItem rosterItem) {
        List<String> resources = registry.getInterestedResources(user);

        for (String resource : resources) {
            Entity userResource = new EntityImpl(user, resource);
            Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource, sessionContext.nextSequenceValue(),
                    rosterItem);
            LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push);
        }
    }

    @SpecCompliance(compliant = {
            @SpecCompliant(spec = "RFC3921bis-05", section = "3.2.3", status = IN_PROGRESS, comment = "current impl based hereupon"),
            @SpecCompliant(spec = "RFC3921bis-08", section = "3.2.3", status = NOT_STARTED, comment = "additions from bis-05 not yet taken into account") })
    protected void handleInboundSubscriptionCancellation(PresenceStanza stanza,
            ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext, ResourceRegistry registry,
            RosterManager rosterManager) {

        Entity contact = stanza.getFrom();
        Entity user = stanza.getTo();

        Entity userBareJid = user.getBareJID();
        Entity contactBareJid = contact.getBareJID();

        RosterItem rosterItem;
        try {
            rosterItem = rosterManager.getContact(userBareJid, contactBareJid);
        } catch (RosterException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        if (rosterItem == null)
            return;

        RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().remove(rosterItem, TO);

        if (result != OK) {
            // TODO
            return;
        }

        // send roster push to all interested resources
        // TODO do this only once, since inbound is multiplexed on DeliveringInboundStanzaRelay level already
        List<String> resources = registry.getInterestedResources(user);
        for (String resource : resources) {
            Entity userResource = new EntityImpl(user, resource);
            Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource, sessionContext.nextSequenceValue(),
                    rosterItem);
            LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push);
        }
    }

    @SpecCompliance(compliant = {
            @SpecCompliant(spec = "RFC3921bis-05", section = "3.2.2", status = IN_PROGRESS, comment = "current impl based hereupon"),
            @SpecCompliant(spec = "RFC3921bis-08", section = "3.2.2", status = NOT_STARTED, comment = "rephrasing from bis-05 not yet taken into account") })
    protected void handleOutboundSubscriptionCancellation(PresenceStanza stanza,
            ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext, ResourceRegistry registry,
            RosterManager rosterManager) {
        Entity user = stanza.getFrom();
        Entity contact = stanza.getTo();

        Entity userBareJid = user.getBareJID();
        Entity contactBareJid = contact.getBareJID();

        RosterItem rosterItem = null;
        try {
            rosterItem = rosterManager.getContact(userBareJid, contactBareJid);
        } catch (RosterException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        if (rosterItem == null)
            return;

        RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().remove(rosterItem, FROM);

        if (result != OK) {
            // TODO
            return;
        }

        relayStanza(contact, stanza, sessionContext);

        // send roster push to all of the user's interested resources
        sendRosterUpdate(sessionContext, registry, user, rosterItem);
    }

    @SpecCompliance(compliant = {
            @SpecCompliant(spec = "RFC3921bis-05", section = "3.1.5", status = IN_PROGRESS, comment = "current impl based hereupon"),
            @SpecCompliant(spec = "RFC3921bis-08", section = "3.1.5", status = NOT_STARTED, comment = "slight changes from bis-05 not yet taken into account") })
    protected void handleOutboundSubscriptionApproval(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext,
            SessionContext sessionContext, ResourceRegistry registry, RosterManager rosterManager) {
        Entity user = stanza.getFrom();
        Entity contact = stanza.getTo();

        Entity userBareJid = user.getBareJID();
        Entity contactBareJid = contact.getBareJID();

        RosterItem rosterItem = null;
        try {
            rosterItem = getExistingOrNewRosterItem(rosterManager, userBareJid, contactBareJid);

            RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().add(rosterItem, FROM);
            if (result != OK) {
                // TODO
                return;
            }

            rosterManager.addContact(userBareJid, rosterItem);
        } catch (RosterException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        relayStanza(contact, stanza, sessionContext);

        // send roster push to all of the user's interested resources
        sendRosterUpdate(sessionContext, registry, user, rosterItem);

        // send presence information from user's available resource to the
        // contact
        List<String> resources = registry.getAvailableResources(user);
        for (String resource : resources) {
            Entity userResource = new EntityImpl(user, resource);

            PresenceStanza cachedPresenceStanza = sessionContext.getServerRuntimeContext().getPresenceCache().get(
                    userResource);
            if (cachedPresenceStanza == null)
                continue;

            PresenceStanza sendoutPresence = buildPresenceStanza(userResource, contactBareJid, null,
                    cachedPresenceStanza.getInnerElements());
            relayStanza(contact, sendoutPresence, sessionContext);
        }
    }

    private RosterItem getExistingOrNewRosterItem(RosterManager rosterManager, Entity userJid, Entity contactJid)
            throws RosterException {
        RosterItem rosterItem = rosterManager.getContact(userJid, contactJid);
        if (rosterItem == null) {
            rosterItem = new RosterItem(contactJid, NONE);
        }
        return rosterItem;
    }

    /**
     * TODO this handling method should be optimized to be processed only once for every session
     *      DeliveringInboundStanzaRelay call this for every resource separately
     */
    @SpecCompliance(compliant = {
            @SpecCompliant(spec = "RFC3921bis-05", section = "3.1.6", status = IN_PROGRESS, comment = "current impl based hereupon"),
            @SpecCompliant(spec = "RFC3921bis-08", section = "3.1.6", status = NOT_STARTED, comment = "minor rephrasing from bis-05 not yet taken into account") })
    protected Stanza handleInboundSubscriptionApproval(PresenceStanza stanza,
            ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext, ResourceRegistry registry,
            RosterManager rosterManager) {

        Entity contact = stanza.getFrom();
        Entity user = stanza.getTo();

        Entity userBareJid = user.getBareJID();

        RosterItem rosterItem;
        RosterSubscriptionMutator.Result result;
        try {
            rosterItem = getExistingOrNewRosterItem(rosterManager, userBareJid, contact);

            result = RosterSubscriptionMutator.getInstance().add(rosterItem, TO);

            rosterManager.addContact(userBareJid, rosterItem);
        } catch (RosterException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        if (result == OK || result == ALREADY_SET) {

            // send roster push to all interested resources
            // TODO do this only once, since inbound is multiplexed on DeliveringInboundStanzaRelay level already
            List<String> resources = registry.getInterestedResources(user);
            for (String resource : resources) {
                Entity userResource = new EntityImpl(user, resource);
                Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource,
                        sessionContext.nextSequenceValue(), rosterItem);
                LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push);
            }
        } else {
            // silently drop the stanza
        }

        return null;
    }

    @SpecCompliance(compliant = {
            @SpecCompliant(spec = "RFC3921bis-05", section = "3.1.3", status = IN_PROGRESS, comment = "current impl based hereupon"),
            @SpecCompliant(spec = "RFC3921bis-08", section = "3.1.3", status = NOT_STARTED, comment = "major rephrasing from bis-05 not yet taken into account") })
    protected Stanza handleInboundSubscriptionRequest(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext,
            SessionContext sessionContext, ResourceRegistry registry, RosterManager rosterManager) {
        Entity contact = stanza.getFrom();
        Entity user = stanza.getTo();

        Entity userBareJid = user.getBareJID();

        RosterItem rosterItem;
        RosterSubscriptionMutator.Result result;
        try {
            rosterItem = getExistingOrNewRosterItem(rosterManager, userBareJid, contact);

            result = RosterSubscriptionMutator.getInstance().add(rosterItem, ASK_SUBSCRIBED);

            rosterManager.addContact(userBareJid, rosterItem);
        } catch (RosterException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        // check whether user already has a subscription to contact
        if (result == ALREADY_SET) {
            Entity receiver = contact.getBareJID();
            PresenceStanza alreadySubscribedResponse = buildPresenceStanza(userBareJid, receiver, SUBSCRIBED, null);
            relayStanza(receiver, alreadySubscribedResponse, sessionContext);
            return null;
        }

        // user exists and doesn't have a subscription, so...

        // TODO check if user has blocked contact

        // write inbound subscription request to the user
        sessionContext.getResponseWriter().write(stanza);

        return null;
    }

    @SpecCompliance(compliant = {
            @SpecCompliant(spec = "RFC3921bis-05", section = "3.1.2", status = IN_PROGRESS, comment = "current impl based hereupon"),
            @SpecCompliant(spec = "RFC3921bis-08", section = "3.1.2", status = NOT_STARTED, comment = "major rephrasing from bis-05 not yet taken into account") })
    protected void handleOutboundSubscriptionRequest(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext,
            SessionContext sessionContext, ResourceRegistry registry, RosterManager rosterManager) {
        StanzaRelay stanzaRelay = serverRuntimeContext.getStanzaRelay();

        Entity user = stanza.getFrom();
        Entity contact = stanza.getTo().getBareJID();

        // TODO schedule a observer which can re-send the request

        RosterItem rosterItem = null;
        try {
            rosterItem = getExistingOrNewRosterItem(rosterManager, user.getBareJID(), contact);

            RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().add(rosterItem,
                    ASK_SUBSCRIBE);
            if (result != OK) {
                return;
            }

            rosterManager.addContact(user.getBareJID(), rosterItem);
        } catch (RosterException e) {
            throw new RuntimeException(e);
        }

        // relay the stanza to the contact (via the contact's server)
        try {
            stanzaRelay.relay(stanza.getTo(), stanza, new IgnoreFailureStrategy());
        } catch (DeliveryException e) {
            e.printStackTrace();
        }

        // send roster push to all of the user's interested resources
        List<String> resources = registry.getInterestedResources(user);
        for (String resource : resources) {
            Entity userResource = new EntityImpl(user, resource);
            Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource, sessionContext.nextSequenceValue(),
                    rosterItem);
            LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push);
        }
    }

}
