/**
 * $RCSfile$
 * $Revision: $
 * $Date: $
 *
 * Copyright (C) 2006 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */
package org.jivesoftware.multiplexer.net.http;

import org.jivesoftware.multiplexer.*;
import org.jivesoftware.multiplexer.spi.ClientFailoverDeliverer;
import org.dom4j.Element;
import org.dom4j.DocumentHelper;

import java.util.*;

/**
 * A session represents a serious of interactions with an XMPP client sending packets using the HTTP
 * Binding protocol specified in
 * <a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>. A session can have several
 * client connections open simultaneously while awaiting packets bound for the client from the
 * server.
 *
 * @author Alexander Wenckus
 */
public class HttpSession extends Session {
    private int wait;
    private int hold = -1000;
    private String language;
    private final Queue<HttpConnection> connectionQueue = new LinkedList<HttpConnection>();
    private final List<Element> pendingElements = new ArrayList<Element>();
    private boolean isSecure;
    private int maxPollingInterval;
    private long lastPoll = -1;
    private Set<SessionListener> listeners = new HashSet<SessionListener>();
    private boolean isClosed;
    private int inactivityTimeout;

    protected HttpSession(String serverName, String streamID) {
        super(serverName, null, streamID);
    }

    void addConnection(HttpConnection connection, boolean isPoll) throws HttpBindException,
            HttpConnectionClosedException
    {
        if(connection == null) {
            throw new IllegalArgumentException("Connection cannot be null.");
        }

        if(isPoll) {
            checkPollingInterval();
        }

        if(isSecure && !connection.isSecure()) {
            throw new HttpBindException("Session was started from secure connection, all " +
                    "connections on this session must be secured.", false, 403);
        }

        connection.setSession(this);
        if (pendingElements.size() > 0) {
            String deliverable = createDeliverable(pendingElements);
            pendingElements.clear();
            fireConnectionOpened(connection);
            connection.deliverBody(deliverable);
            fireConnectionClosed(connection);
        }
        else {
            // With this connection we need to check if we will have too many connections open,
            // closing any extras.
            while (hold > 0 && connectionQueue.size() >= hold) {
                HttpConnection toClose = connectionQueue.remove();
                toClose.close();
                fireConnectionClosed(toClose);
            }
            connectionQueue.offer(connection);
            fireConnectionOpened(connection);
        }
    }

    private void fireConnectionOpened(HttpConnection connection) {
        Collection<SessionListener> listeners =
                new HashSet<SessionListener>(this.listeners);
        for(SessionListener listener : listeners) {
            listener.connectionOpened(this, connection);
        }
    }

    private void checkPollingInterval() throws HttpBindException {
        long time = System.currentTimeMillis();
        if(lastPoll > 0  && ((time - lastPoll) / 1000) < maxPollingInterval) {
            throw new HttpBindException("Too frequent polling minimum interval is "
                    + maxPollingInterval + ", current interval " + ((lastPoll - time) / 1000),
                    true, 403);
        }
        lastPoll = time;
    }

    public String getAvailableStreamFeatures() {
        return null;
    }

    public synchronized void close() {
        close(false);
    }

    public synchronized void close(boolean isServerShuttingDown) {
        if(isClosed) {
            return;
        }
        isClosed = true;

        if(pendingElements.size() > 0) {
            failDelivery();
        }

        Collection<SessionListener> listeners =
                new HashSet<SessionListener>(this.listeners);
        this.listeners.clear();
        for(SessionListener listener : listeners) {
            listener.sessionClosed(this);
        }
    }

    private void failDelivery() {
        ClientFailoverDeliverer deliverer = new ClientFailoverDeliverer();
        deliverer.setStreamID(getStreamID());
        for(Element element : pendingElements) {
            deliverer.deliver(element);
        }
        pendingElements.clear();
    }

    public synchronized boolean isClosed() {
        return isClosed;
    }

    public synchronized void deliver(Element stanza) {
        String deliverable = createDeliverable(Arrays.asList(stanza));
        boolean delivered = false;
        while(!delivered && connectionQueue.size() > 0) {
            HttpConnection connection = connectionQueue.remove();
            try {
                connection.deliverBody(deliverable);
                delivered = true;
                fireConnectionClosed(connection);
            }
            catch (HttpConnectionClosedException e) {
                /* Connection was closed, try the next one */
            }
        }

        if(!delivered) {
            pendingElements.add(stanza);
        }
    }

    private void fireConnectionClosed(HttpConnection connection) {
        Collection<SessionListener> listeners =
                new HashSet<SessionListener>(this.listeners);
        for(SessionListener listener : listeners) {
            listener.connectionClosed(this, connection);
        }
    }

    private String createDeliverable(Collection<Element> elements) {
        Element body = DocumentHelper.createElement("body");
        body.addNamespace("", "http://jabber.org/protocol/httpbind");
        for(Element child : elements) {
            child = child.createCopy();
            child.setParent(null);
            body.add(child);
        }
        return body.asXML();
    }

    /**
     * This attribute specifies the longest time (in seconds) that the connection manager is allowed
     * to wait before responding to any request during the session. This enables the client to
     * prevent its TCP connection from expiring due to inactivity, as well as to limit the delay
     * before it discovers any network failure.
     *
     * @param wait the longest time it is permissible to wait for a response.
     */
    public void setWait(int wait) {
        this.wait = wait;
    }

    /**
     * This attribute specifies the longest time (in seconds) that the connection manager is allowed
     * to wait before responding to any request during the session. This enables the client to
     * prevent its TCP connection from expiring due to inactivity, as well as to limit the delay
     * before it discovers any network failure.
     *
     * @return the longest time it is permissible to wait for a response.
     */
    public int getWait() {
        return wait;
    }

    /**
     * This attribute specifies the maximum number of requests the connection manager is allowed
     * to keep waiting at any one time during the session. (For example, if a constrained client
     * is unable to keep open more than two HTTP connections to the same HTTP server simultaneously,
     * then it SHOULD specify a value of "1".)
     *
     * @param hold the maximum number of simultaneous waiting requests.
     *
     */
    public void setHold(int hold) {
        this.hold = hold;
    }

    /**
     * This attribute specifies the maximum number of requests the connection manager is allowed
     * to keep waiting at any one time during the session. (For example, if a constrained client
     * is unable to keep open more than two HTTP connections to the same HTTP server simultaneously,
     * then it SHOULD specify a value of "1".)
     *
     * @return the maximum number of simultaneous waiting requests
     */
    public int getHold() {
        return hold;
    }

    public void setLanaguage(String language) {
        this.language = language;
    }

    public String getLanguage() {
        return language;
    }

    /**
     * Sets the max interval within which a client can send polling requests. If more than one
     * request occurs in the interval the session will be terminated.
     *
     * @param maxPollingInterval time in seconds a client needs to wait before sending polls to the
     * server, a negative <i>int</i> indicates that there is no limit.
     */
    public void setMaxPollingInterval(int maxPollingInterval) {
        this.maxPollingInterval = maxPollingInterval;
    }

    /**
     * Sets whether the initial request on the session was secure.
     *
     * @param isSecure true if the initial request was secure and false if it wasn't.
     */
    protected void setSecure(boolean isSecure) {
        this.isSecure = isSecure;
    }

    /**
     * Returns true if all connections on this session should be secured, and false if
     * they should not.
     *
     * @return true if all connections on this session should be secured, and false if
     * they should not.
     */
    public boolean isSecure() {
        return isSecure;
    }

    public void addSessionCloseListener(SessionListener listener) {
        listeners.add(listener);
    }

    public void removeSessionCloseListener(SessionListener listener) {
        listeners.remove(listener);
    }

    public void setInactivityTimeout(int inactivityTimeout) {
        this.inactivityTimeout = inactivityTimeout;
    }

    public int getInactivityTimeout() {
        return inactivityTimeout;
    }

    public int getConnectionCount() {
        return connectionQueue.size();
    }
}
