/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed 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 net.java.sip.communicator.service.protocol;

import java.io.*;
import java.nio.charset.*;
import java.util.*;

import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;

/**
 * Represents a default implementation of
 * {@link OperationSetBasicInstantMessaging} in order to make it easier for
 * implementers to provide complete solutions while focusing on
 * implementation-specific details.
 *
 * @author Lyubomir Marinov
 */
public abstract class AbstractOperationSetBasicInstantMessaging
    implements OperationSetBasicInstantMessaging
{
    /**
     * The <tt>Logger</tt> used by the
     * <tt>AbstractOperationSetBasicInstantMessaging</tt> class and its
     * instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(AbstractOperationSetBasicInstantMessaging.class);

    /**
     * A list of listeners registered for message events.
     */
    private final List<MessageListener> messageListeners =
        new LinkedList<MessageListener>();

    /**
     * Registers a MessageListener with this operation set so that it gets
     * notifications of successful message delivery, failure or reception of
     * incoming messages..
     *
     * @param listener the <tt>MessageListener</tt> to register.
     */
    public void addMessageListener(MessageListener listener)
    {
        synchronized (messageListeners)
        {
            if (!messageListeners.contains(listener))
            {
                messageListeners.add(listener);
            }
        }
    }

    /**
     * Create a Message instance for sending arbitrary MIME-encoding content.
     *
     * @param content content value
     * @param contentType the MIME-type for <tt>content</tt>
     * @param encoding encoding used for <tt>content</tt>
     * @param subject a <tt>String</tt> subject or <tt>null</tt> for now
     *            subject.
     * @return the newly created message.
     */
    public Message createMessage(byte[] content, String contentType,
        String encoding, String subject)
    {
        String contentAsString = null;
        boolean useDefaultEncoding = true;
        if (encoding != null)
        {
            try
            {
                contentAsString = new String(content, encoding);
                useDefaultEncoding = false;
            }
            catch (UnsupportedEncodingException ex)
            {
                logger.warn("Failed to decode content using encoding "
                    + encoding, ex);

                // We'll use the default encoding.
            }
        }
        if (useDefaultEncoding)
        {
            encoding = Charset.defaultCharset().name();
            contentAsString = new String(content);
        }

        return createMessage(contentAsString, contentType, encoding, subject);
    }

    /**
     * Create a Message instance for sending a simple text messages with default
     * (text/plain) content type and encoding.
     *
     * @param messageText the string content of the message.
     * @return Message the newly created message
     */
    public Message createMessage(String messageText)
    {
        return createMessage(messageText, DEFAULT_MIME_TYPE,
            DEFAULT_MIME_ENCODING, null);
    }

    /**
     * Create a Message instance with the specified UID, content type
     * and a default encoding.
     * This method can be useful when message correction is required. One can
     * construct the corrected message to have the same UID as the message
     * before correction.
     *
     * @param messageText the string content of the message.
     * @param contentType the MIME-type for <tt>content</tt>
     * @param messageUID the unique identifier of this message.
     * @return Message the newly created message
     */
    public Message createMessageWithUID(
        String messageText, String contentType, String messageUID)
    {
        return createMessage(messageText);
    }

    public abstract Message createMessage(
        String content, String contentType, String encoding, String subject);

    /**
     * Notifies all registered message listeners that a message has been
     * delivered successfully to its addressee..
     *
     * @param message the <tt>Message</tt> that has been delivered.
     * @param to the <tt>Contact</tt> that <tt>message</tt> was delivered to.
     */
    protected void fireMessageDelivered(Message message, Contact to)
    {
        fireMessageEvent(
            new MessageDeliveredEvent(message, to, new Date()));
    }

    protected void fireMessageDeliveryFailed(
        Message message,
        Contact to,
        int errorCode)
    {
        fireMessageEvent(
            new MessageDeliveryFailedEvent(message, to, errorCode));
    }

    enum MessageEventType{
        None,
        MessageDelivered,
        MessageReceived,
        MessageDeliveryFailed,
        MessageDeliveryPending,
    }

    /**
     * Delivers the specified event to all registered message listeners.
     *
     * @param evt the <tt>EventObject</tt> that we'd like delivered to all
     *            registered message listeners.
     */
    protected void fireMessageEvent(EventObject evt)
    {
        Collection<MessageListener> listeners = null;
        synchronized (this.messageListeners)
        {
            listeners = new ArrayList<MessageListener>(this.messageListeners);
        }

        if (logger.isDebugEnabled())
            logger.debug("Dispatching Message Listeners=" + listeners.size()
            + " evt=" + evt);

        /*
         * TODO Create a super class like this MessageEventObject that would
         * contain the MessageEventType. Also we could fire an event for the
         * MessageDeliveryPending event type (modify MessageListener and
         * OperationSetInstantMessageTransform).
         */
        MessageEventType eventType = MessageEventType.None;
        if (evt instanceof MessageDeliveredEvent)
        {
            eventType = MessageEventType.MessageDelivered;
        }
        else if (evt instanceof MessageReceivedEvent)
        {
            eventType = MessageEventType.MessageReceived;
        }
        else if (evt instanceof MessageDeliveryFailedEvent)
        {
            eventType = MessageEventType.MessageDeliveryFailed;
        }

        // Transform the event.
        EventObject[] events = messageTransform(evt, eventType);
        for (EventObject event : events)
        {
            try
            {
                if (event == null)
                    return;

                for (MessageListener listener : listeners)
                {
                    switch (eventType)
                    {
                    case MessageDelivered:
                        listener.messageDelivered((MessageDeliveredEvent) event);
                        break;
                    case MessageDeliveryFailed:
                        listener
                            .messageDeliveryFailed((MessageDeliveryFailedEvent) event);
                        break;
                    case MessageReceived:
                        listener.messageReceived((MessageReceivedEvent) event);
                        break;
                    default:
                        /*
                         * We either have nothing to do or we do not know what
                         * to do. Anyway, we'll silence the compiler.
                         */
                        break;
                    }
                }
            }
            catch (Throwable e)
            {
                logger.error("Error delivering message", e);
            }
        }
    }

    /**
     * Notifies all registered message listeners that a message has been
     * received.
     *
     * @param message the <tt>Message</tt> that has been received.
     * @param from the <tt>Contact</tt> that <tt>message</tt> was received from.
     */
    protected void fireMessageReceived(Message message, Contact from)
    {
        fireMessageEvent(
            new MessageReceivedEvent(message, from, new Date()));
    }

    /**
     * Unregisters <tt>listener</tt> so that it won't receive any further
     * notifications upon successful message delivery, failure or reception of
     * incoming messages..
     *
     * @param listener the <tt>MessageListener</tt> to unregister.
     */
    public void removeMessageListener(MessageListener listener)
    {
        synchronized (messageListeners)
        {
            messageListeners.remove(listener);
        }
    }

    /**
     * Messages pending delivery to be transformed.
     *
     * @param evt the message delivery event
     * @return returns message delivery events
     */
    public MessageDeliveredEvent[] messageDeliveryPendingTransform(
            final MessageDeliveredEvent evt)
    {
        EventObject[] transformed = messageTransform(
            evt, MessageEventType.MessageDeliveryPending);

        final int size = transformed.length;
        MessageDeliveredEvent[] events =
            new MessageDeliveredEvent[size];
        System.arraycopy(transformed, 0, events, 0, size);
        return events;
    }

    /**
     * Transform provided source event by processing transform layers in
     * sequence.
     *
     * @param evt the source event to transform
     * @param eventType the event type of the source event
     * @return returns the resulting (transformed) events, if any. (I.e. an
     *         array of 0 or more size containing events.)
     */
    private EventObject[] messageTransform(final EventObject evt,
                                           final MessageEventType eventType)
    {
        if (evt == null)
        {
            return new EventObject[0];
        }
        ProtocolProviderService protocolProvider;
        switch (eventType)
        {
        case MessageDelivered:
            protocolProvider
                = ((MessageDeliveredEvent) evt)
                    .getDestinationContact().getProtocolProvider();
            break;
        case MessageDeliveryFailed:
            protocolProvider
                = ((MessageDeliveryFailedEvent) evt)
                    .getDestinationContact().getProtocolProvider();
            break;
        case MessageDeliveryPending:
            protocolProvider
                = ((MessageDeliveredEvent) evt)
                    .getDestinationContact().getProtocolProvider();
            break;
        case MessageReceived:
            protocolProvider
                = ((MessageReceivedEvent) evt)
                    .getSourceContact().getProtocolProvider();
            break;
        default:
            return new EventObject[] {evt};
        }

        OperationSetInstantMessageTransformImpl opSetMessageTransform
            = (OperationSetInstantMessageTransformImpl) protocolProvider
                .getOperationSet(OperationSetInstantMessageTransform.class);

        if (opSetMessageTransform == null)
            return new EventObject[] {evt};

        // 'current' contains the events that need to be transformed. It should
        // not contain null values.
        final LinkedList<EventObject> current = new LinkedList<EventObject>();
        // Add source event as start of transformation.
        current.add(evt);
        // 'next' contains the resulting events after transformation in the
        // current iteration. It should not contain null values.
        final LinkedList<EventObject> next = new LinkedList<EventObject>();
        for (Map.Entry<Integer, Vector<TransformLayer>> entry
                : opSetMessageTransform.transformLayers.entrySet())
        {
            for (TransformLayer transformLayer : entry.getValue())
            {
                next.clear();
                while (!current.isEmpty())
                {
                    final EventObject event = current.remove();
                    switch (eventType)
                    {
                    case MessageDelivered:
                        MessageDeliveredEvent transformedDelivered =
                            transformLayer.messageDelivered(
                                (MessageDeliveredEvent) event);
                        if (transformedDelivered != null)
                        {
                            next.add(transformedDelivered);
                        }
                        break;
                    case MessageDeliveryPending:
                        MessageDeliveredEvent[] evts = transformLayer
                            .messageDeliveryPending(
                                (MessageDeliveredEvent) event);
                        for (MessageDeliveredEvent mde : evts)
                        {
                            if (mde != null)
                            {
                                next.add(mde);
                            }
                        }
                        break;
                    case MessageDeliveryFailed:
                        MessageDeliveryFailedEvent transformedDeliveryFailed =
                            transformLayer.messageDeliveryFailed(
                                (MessageDeliveryFailedEvent) event);
                        if (transformedDeliveryFailed != null)
                        {
                            next.add(transformedDeliveryFailed);
                        }
                        break;
                    case MessageReceived:
                        MessageReceivedEvent transformedReceived =
                            transformLayer
                                .messageReceived((MessageReceivedEvent) event);
                        if (transformedReceived != null)
                        {
                            next.add(transformedReceived);
                        }
                        break;
                    default:
                        next.add(event);
                        /*
                         * We either have nothing to do or we do not know
                         * what to do. Anyway, we'll silence the compiler.
                         */
                        break;
                    }
                }
                // Set events for next round of transformations.
                current.addAll(next);
            }
        }

        return current.toArray(new EventObject[current.size()]);
    }

    /**
     * Determines whether the protocol supports the supplied content type
     * for the given contact.
     *
     * @param contentType the type we want to check
     * @param contact contact which is checked for supported contentType
     * @return <tt>true</tt> if the contact supports it and
     * <tt>false</tt> otherwise.
     */
    public boolean isContentTypeSupported(String contentType, Contact contact)
    {
        // by default we support default mime type, for other mime-types
        // method must be overridden
        if(contentType.equals(DEFAULT_MIME_TYPE))
            return true;

        return false;
    }

    /**
     * Sends the <tt>message</tt> to the destination indicated by the
     * <tt>to</tt>. Provides a default implementation of this method.
     *
     * @param to the <tt>Contact</tt> to send <tt>message</tt> to
     * @param toResource the resource to which the message should be send
     * @param message the <tt>Message</tt> to send.
     * @throws java.lang.IllegalStateException if the underlying ICQ stack is
     * not registered and initialized.
     * @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an
     * instance belonging to the underlying implementation.
     */
    public void sendInstantMessage( Contact to,
                                    ContactResource toResource,
                                    Message message)
        throws IllegalStateException,
               IllegalArgumentException, OperationFailedException
    {
        sendInstantMessage(to, message);
    }

    /**
     * Returns the inactivity timeout in milliseconds.
     *
     * @return The inactivity timeout in milliseconds. Or -1 if undefined
     */
    public long getInactivityTimeout()
    {
        return -1;
    }
}
