/*
 * ICESOFT COMMERCIAL SOURCE CODE LICENSE V 1.1
 *
 * The contents of this file are subject to the ICEsoft Commercial Source
 * Code License Agreement V1.1 (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.icesoft.com/license/commercial-source-v1.1.html
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License.
 *
 * Copyright 2009-2015 ICEsoft Technologies Canada, Corp. All Rights Reserved.
 */
package com.icesoft.push.messaging;

import com.icesoft.net.messaging.AbstractMessageHandler;
import com.icesoft.net.messaging.DefaultMessageService;
import com.icesoft.net.messaging.Message;
import com.icesoft.net.messaging.MessageHandler;
import com.icesoft.net.messaging.MessageSelector;
import com.icesoft.net.messaging.MessageServiceClient;
import com.icesoft.net.messaging.MessageServiceException;
import com.icesoft.net.messaging.ObjectMessage;
import com.icesoft.net.messaging.TextMessage;
import com.icesoft.net.messaging.expression.And;
import com.icesoft.net.messaging.expression.Equal;
import com.icesoft.net.messaging.expression.Identifier;
import com.icesoft.net.messaging.expression.NotEqual;
import com.icesoft.net.messaging.expression.Or;
import com.icesoft.net.messaging.expression.StringLiteral;
import com.icesoft.net.messaging.expression.Container;
import com.icesoft.net.messaging.expression.IsNull;
import com.icesoft.push.LocalPushGroupManager;
import com.icesoft.push.PushID;
import com.icesoft.util.Properties;
import com.icesoft.push.ProductInfo;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.icepush.Configuration;
import org.icepush.NotifyBackURI;
import org.icepush.PushConfiguration;
import org.icepush.PushContext;

public class PushMessageService
extends DefaultMessageService {
    private static final Logger LOGGER = Logger.getLogger(PushMessageService.class.getName());

    public static final String ANNOUNCEMENT_MESSAGE_TYPE = "Announcement";
    public static final String HELLO_MESSAGE_TYPE = "Hello";
    public static final String ACKNOWLEDGE_MESSAGE_TYPE = "Acknowledge";
    public static final String ADD_GROUP_MEMBER_MESSAGE_TYPE = "AddGroupMember";
    public static final String CLEAR_PENDING_NOTIFICATIONS_MESSAGE_TYPE = "ClearPendingNotifications";
    public static final String PUSH_MESSAGE_TYPE = "Push";
    public static final String SET_NOTIFY_BACK_URI_MESSAGE_TYPE = "SetNotifyBackURI";
    public static final String BACK_OFF_MESSAGE_TYPE = "BackOff";
    public static final String REMOVE_GROUP_MEMBER_MESSAGE_TYPE = "RemoveGroupMember";
    public static final String STATUS_MESSAGE_TYPE = "Status";
    public static final String SHUTDOWN_MESSAGE_TYPE = "Shutdown";

    private static final String PRODUCT_MESSAGE =
        ProductInfo.PRODUCT + "," +
        ProductInfo.PRIMARY + "," +
        ProductInfo.SECONDARY + "," +
        ProductInfo.TERTIARY + "," +
        ProductInfo.RELEASE_TYPE + "," +
        ProductInfo.BUILD_NO + "," +
        ProductInfo.REVISION;

    private static String HOST_ADDRESS;
    {
        try {
            HOST_ADDRESS = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException exception) {
            if (LOGGER.isLoggable(Level.SEVERE)) {
                LOGGER.log(Level.SEVERE, "An error occurred while trying to get the local host.", exception);
            }
            HOST_ADDRESS = null;
        }
    }

    private final MessageHandler announcementMessageHandler;
    private final MessageHandler helloMessageHandler;
    private final MessageHandler addGroupMemberMessageHandler;
    private final MessageHandler clearPendingNotificationsMessageHandler;
    private final MessageHandler pushMessageHandler;
    private final MessageHandler setNotifyBackURIMessageHandler;
    private final MessageHandler backOffMessageHandler;
    private final MessageHandler removeGroupMemberMessageHandler;
    private final MessageHandler statusMessageHandler;
    private final MessageHandler shutdownMessageHandler;

    public PushMessageService(
        final PushContext pushContext, final MessageServiceClient messageServiceClient,
        final ScheduledThreadPoolExecutor executor, final Configuration configuration, final boolean retryOnFail,
        final LocalPushGroupManager localPushGroupManager) {

        super(messageServiceClient, executor, configuration, retryOnFail);
        announcementMessageHandler =
            new AbstractMessageHandler(
                new MessageSelector(
                    new And(
                        new Equal(
                            new Identifier(Message.MESSAGE_TYPE),
                            new StringLiteral(ANNOUNCEMENT_MESSAGE_TYPE)),
                        new NotEqual(
                            new Identifier(Message.SOURCE_NODE_ADDRESS),
                            new StringLiteral(HOST_ADDRESS))))) {

                public void handle(final Message message) {
                    if (message != null && message instanceof TextMessage) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Handling message:\r\n\r\n" + message);
                        }
                    }
                    Properties _messageProperties = new Properties();
                    _messageProperties.setStringProperty(
                        Message.DESTINATION_SERVLET_CONTEXT_PATH,
                        message.getStringProperty(Message.SOURCE_SERVLET_CONTEXT_PATH));
                    Map<String, String[]> _groupMap = localPushGroupManager.getGroupMap();
                    for (String groupName : _groupMap.keySet()) {
                        String[] _pushIDs = _groupMap.get(groupName);
                        for (String pushID : _pushIDs) {
                            MessagePayload _messagePayload = new MessagePayload();
                            _messagePayload.setGroupName(groupName);
                            _messagePayload.setPushID((PushID)localPushGroupManager.getPushID(pushID));
                            getMessageServiceClient().publish(
                                _messagePayload,
                                _messageProperties,
                                "AddGroupMember");
                        }
                    }
                }
            };
        helloMessageHandler =
            new AbstractMessageHandler(
                new MessageSelector(
                    new Equal(new Identifier(Message.MESSAGE_TYPE), new StringLiteral(HELLO_MESSAGE_TYPE)))) {

                public void handle(final Message message) {
                    if (message != null && message instanceof TextMessage) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Handling message:\r\n\r\n" + message);
                        }
                        Properties _messageProperties = new Properties();
                        _messageProperties.setStringProperty(
                            Message.DESTINATION_SERVLET_CONTEXT_PATH,
                            message.getStringProperty(Message.SOURCE_SERVLET_CONTEXT_PATH));
                        getMessageServiceClient().publish(
                            PRODUCT_MESSAGE, _messageProperties, ACKNOWLEDGE_MESSAGE_TYPE);
                    }
                }
            };
        addGroupMemberMessageHandler =
            new AbstractMessageHandler(
                new MessageSelector(
                    new And(
                        new Equal(
                            new Identifier(Message.MESSAGE_TYPE),
                            new StringLiteral(ADD_GROUP_MEMBER_MESSAGE_TYPE)),
                        new Container(
                            new Or(
                                new IsNull(
                                    new Identifier(Message.DESTINATION_NODE_ADDRESS)),
                                new Equal(
                                    new Identifier(Message.DESTINATION_NODE_ADDRESS),
                                    new StringLiteral(HOST_ADDRESS))))))) {

                public void handle(final Message message) {
                    if (message != null && message instanceof ObjectMessage) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Handling message:\r\n\r\n" + message);
                        }
                        /*
                         * message-body:
                         *     MessagePayload
                         */
                        MessagePayload _messagePayload = (MessagePayload)((ObjectMessage)message).getObject();
                        PushID _pushID = _messagePayload.getPushID();
                        if (_pushID == null) {
                            localPushGroupManager.addMember(
                                _messagePayload.getGroupName(), _messagePayload.getPushIDString());
                        } else {
                            localPushGroupManager.addMember(
                                _messagePayload.getGroupName(), _messagePayload.getPushID());
                        }
                    }
                }
            };
        clearPendingNotificationsMessageHandler =
            new AbstractMessageHandler(
                new MessageSelector(
                    new Equal(
                        new Identifier(Message.MESSAGE_TYPE),
                        new StringLiteral(CLEAR_PENDING_NOTIFICATIONS_MESSAGE_TYPE)))) {

                public void handle(final Message message) {
                    if (message != null && message instanceof ObjectMessage) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Handling message:\r\n\r\n" + message);
                        }
                        /*
                         * message-body:
                         *     MessagePayload
                         */
                        MessagePayload _messagePayload = (MessagePayload)((ObjectMessage)message).getObject();
                        localPushGroupManager.clearPendingNotifications(_messagePayload.getPushIDSet());
                    }
                }
            };
        pushMessageHandler =
            new AbstractMessageHandler(
                new MessageSelector(
                    new Equal(
                        new Identifier(Message.MESSAGE_TYPE),
                        new StringLiteral(PUSH_MESSAGE_TYPE)))) {

                public void handle(final Message message) {
                    if (message != null && message instanceof ObjectMessage) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Handling message:\r\n\r\n" + message);
                        }
                        /*
                         * message-body:
                         *     MessagePayload
                         */
                        MessagePayload _messagePayload = (MessagePayload)((ObjectMessage)message).getObject();
                        PushConfiguration _pushConfiguration = _messagePayload.getPushConfiguration();
                        if (_pushConfiguration == null) {
                            pushContext.push(_messagePayload.getGroupName());
                        } else {
                            pushContext.push(_messagePayload.getGroupName(), _pushConfiguration);
                        }
                    }
                }
            };
        setNotifyBackURIMessageHandler =
            new AbstractMessageHandler(
                new MessageSelector(
                    new And(
                        new Equal(
                            new Identifier(Message.MESSAGE_TYPE),
                            new StringLiteral(SET_NOTIFY_BACK_URI_MESSAGE_TYPE)),
                        new NotEqual(
                            new Identifier(Message.SOURCE_NODE_ADDRESS),
                            new StringLiteral(HOST_ADDRESS))))) {

                public void handle(final Message message) {
                    if (message != null && message instanceof ObjectMessage) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Handling message:\r\n\r\n" + message);
                        }
                        /*
                         * message-body:
                         *     MessagePayload
                         */
                        MessagePayload _messagePayload = (MessagePayload)((ObjectMessage)message).getObject();
                        ((LocalPushGroupManager)localPushGroupManager).
                            setNotifyBackURI(
                                _messagePayload.getPushIDSet(),
                                new NotifyBackURI(_messagePayload.getNotifyBackURI()),
                                false);
                    }
                }
            };
        backOffMessageHandler =
            new AbstractMessageHandler(
                new MessageSelector(
                    new Equal(
                        new Identifier(Message.MESSAGE_TYPE),
                        new StringLiteral(BACK_OFF_MESSAGE_TYPE)))) {

                public void handle(final Message message) {
                    if (message != null && message instanceof ObjectMessage) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Handling message:\r\n\r\n" + message);
                        }
                        /*
                         * message-body:
                         *     MessagePayload
                         */
                        MessagePayload _messagePayload = (MessagePayload)((ObjectMessage)message).getObject();
                        pushContext.backOff(_messagePayload.getBrowserID(), _messagePayload.getDelay());
                    }
                }
            };
        removeGroupMemberMessageHandler =
            new AbstractMessageHandler(
                new MessageSelector(
                    new Equal(
                        new Identifier(Message.MESSAGE_TYPE),
                        new StringLiteral(REMOVE_GROUP_MEMBER_MESSAGE_TYPE)))) {

                public void handle(final Message message) {
                    if (message != null && message instanceof ObjectMessage) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Handling message:\r\n\r\n" + message);
                        }
                        /*
                         * message-body:
                         *     MessagePayload
                         */
                        MessagePayload _messagePayload = (MessagePayload)((ObjectMessage)message).getObject();
                        pushContext.removeGroupMember(
                            _messagePayload.getGroupName(), _messagePayload.getPushIDString());
                    }
                }
            };
        statusMessageHandler =
            new AbstractMessageHandler(
                new MessageSelector(
                    new And(
                        new Equal(
                            new Identifier(Message.MESSAGE_TYPE),
                            new StringLiteral(STATUS_MESSAGE_TYPE)),
                        new NotEqual(
                            new Identifier(Message.SOURCE_NODE_ADDRESS),
                            new StringLiteral(HOST_ADDRESS))))) {

                public void handle(final Message message) {
                    if (message != null && message instanceof ObjectMessage) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Handling message:\r\n\r\n" + message);
                        }
                        /*
                         * message-body:
                         *     MessagePayload
                         */
                        MessagePayload _messagePayload = (MessagePayload)((ObjectMessage)message).getObject();
                        localPushGroupManager.handleStatus(
                                message.getStringProperty(Message.SOURCE_NODE_ADDRESS), _messagePayload.getUUID(),
                                _messagePayload.getTimestamp(),
                                _messagePayload.getPushIDStatusMap());
                    }
                }
            };
        shutdownMessageHandler =
            new AbstractMessageHandler(
                new MessageSelector(
                    new And(
                        new Equal(
                            new Identifier(Message.MESSAGE_TYPE),
                            new StringLiteral(SHUTDOWN_MESSAGE_TYPE)),
                        new NotEqual(
                            new Identifier(Message.SOURCE_NODE_ADDRESS),
                            new StringLiteral(HOST_ADDRESS))))) {

                public void handle(final Message message) {
                    if (message != null && message instanceof ObjectMessage) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "Handling message:\r\n\r\n" + message);
                        }
                        /*
                         * message-body:
                         *     MessagePayload
                         */
                        MessagePayload _messagePayload = (MessagePayload)((ObjectMessage)message).getObject();
                        localPushGroupManager.handleShutdown(_messagePayload.getUUID());
                    }
                }
            };
    }

    protected void setUpMessageServiceClient()
    throws MessageServiceException {
        MessageServiceClient _messageServiceClient = getMessageServiceClient();
        // throws MessageServiceException
        _messageServiceClient.subscribe(
            new MessageSelector(
                new Or(
                    announcementMessageHandler.getMessageSelector().getExpression(),
                    helloMessageHandler.getMessageSelector().getExpression(),
                    addGroupMemberMessageHandler.getMessageSelector().getExpression(),
                    clearPendingNotificationsMessageHandler.getMessageSelector().getExpression(),
                    pushMessageHandler.getMessageSelector().getExpression(),
                    setNotifyBackURIMessageHandler.getMessageSelector().getExpression(),
                    backOffMessageHandler.getMessageSelector().getExpression(),
                    removeGroupMemberMessageHandler.getMessageSelector().getExpression(),
                    statusMessageHandler.getMessageSelector().getExpression(),
                    shutdownMessageHandler.getMessageSelector().getExpression())));
        _messageServiceClient.addMessageHandlers(
            announcementMessageHandler,
            helloMessageHandler,
            addGroupMemberMessageHandler,
            clearPendingNotificationsMessageHandler,
            pushMessageHandler,
            setNotifyBackURIMessageHandler,
            backOffMessageHandler,
            removeGroupMemberMessageHandler,
            statusMessageHandler,
            shutdownMessageHandler);
        Properties _messageProperties = new Properties();
        _messageServiceClient.publishNow(PRODUCT_MESSAGE, _messageProperties, ANNOUNCEMENT_MESSAGE_TYPE);
    }

    protected void tearDownMessageServiceClient()
    throws MessageServiceException {
        getMessageServiceClient().removeMessageHandlers(
            shutdownMessageHandler,
            statusMessageHandler,
            removeGroupMemberMessageHandler,
            backOffMessageHandler,
            setNotifyBackURIMessageHandler,
            pushMessageHandler,
            clearPendingNotificationsMessageHandler,
            addGroupMemberMessageHandler,
            helloMessageHandler,
            announcementMessageHandler);
        // throws MessageServiceException
        getMessageServiceClient().unsubscribe();
    }
}
