/*
 * Copyright (C) 2004-2008 Jive Software. All rights reserved.
 *
 * 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 org.jivesoftware.xmpp.workgroup;

import org.jivesoftware.xmpp.workgroup.request.Request;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * <p>A simple, in-memory implementation of an Offer.</p>
 * <p/>
 * <p>Offers are not designed to survive the shutdown of the server.</p>
 *
 * @author Derek DeMoro
 */
public class Offer {
    /**
     * User associated with this Offer
     */
    private Request request;
    /**
     * RequestQueue that is sending this offer to the queue's agents.
     */
    private RequestQueue queue;
    /**
     * AgentSessions that received this offer and have not yet answered. Once the invitation has
     * been sent this list will be cleared.
     */
    private List<AgentSession> pendingSessions = new CopyOnWriteArrayList<AgentSession>();
    /**
     * AgentSessions that accepted this offer and one of them will be choosen to start a
     * conversation. Once the invitation has been sent this list will be cleared.
     */
    private List<AgentSession> acceptedSessions = new CopyOnWriteArrayList<AgentSession>();
    /**
     * The (server) time the Offer was made to the agent, initialized in constructor.
     */
    private Date offerTime;
    /**
     * The timeout of the Offer in milliseconds or -1 for no timeout.
     */
    private long timeout = 20000;
    /**
     * The set of agents that have rejected this Offer.
     */
    private Set<String> rejections = new HashSet<String>();
    // ########################################################################
    /**
     * <p>Temporary auto re-offer timeouts for rejectors.</p>
     * <p/>
     * <p>Matt wants agent rejectors to be automatically re-offered
     * offers after a timeout regardless of the agent's actions.
     * The motivation is to ensure offers don't stay idle in the queue
     * waiting for an agent to change their status to get another offer.</p>
     * <p/>
     * <p>The proper behavior shoud be to have the agent app notifyEvent the server
     * when the agent is ready to accept new offers (or be re-offered old ones)
     * at user defined times. Manually is probably preferred (press button on
     * app to indicate "ready for next". Or could be automatic via a timer.
     * In either case driving this from the server is a poor solution
     * and should be fixed ASAP.</p>
     */
    private Map<String, Date> rejectionTimes = new HashMap<String, Date>();

    /**
     * The length in milliseconds before automatically removing an agent
     * from the rejector list.
     */
    private long rejectionTimeout;

    /**
     * Flag indicating the offer has been cancelled or not.
     */
    private boolean cancelled;

    /**
     * Flag indicating an invitation was sent to an agent or not. An invitation will be sent after
     * an agent accepted the offer and was chosen to atend the user's request.
     */
    private boolean invitationSent;

    /**
     * Defined States *
     */
    public static final int USER_CANCELLED = 0;
    public static final int ROUTE_EXPIRED = 1;
    public static final int ROUTED = 2;


    /**
     * Create an offer based on the request.
     *
     * @param request The request this offer is for
     * @param queue   The request queue that is sending this offer to the agents.
     * @param rejectionTimeout the number of milliseconds to wait until expiring an agent rejection.
     */
    public Offer(Request request, RequestQueue queue, long rejectionTimeout) {
        this.request = request;
        this.queue = queue;
        this.rejectionTimeout = rejectionTimeout;
        offerTime = new Date();
        cancelled = false;
        invitationSent = false;
        request.setOffer(this);
    }

    public Request getRequest() {
        return request;
    }

    public boolean isAccepted() {
        return !acceptedSessions.isEmpty();
    }

    public boolean isCancelled() {
        return cancelled;
    }

    public void accept(AgentSession agentSession) {
        acceptedSessions.add(agentSession);
        pendingSessions.remove(agentSession);
    }

    public void reject(AgentSession agentSession) {
        if (pendingSessions.contains(agentSession)) {
            addRejector(agentSession);
            pendingSessions.remove(agentSession);
            agentSession.removeOffer(this);
        }
    }

    private void addRejector(AgentSession agentSession) {
        rejections.add(agentSession.getJID().toBareJID());
        rejectionTimes.put(agentSession.getJID().toBareJID(), new Date());
    }

    public void removeRejector(AgentSession agentSession) {
        rejections.remove(agentSession.getJID().toBareJID());
        rejectionTimes.remove(agentSession.getJID().toBareJID());
    }

    public boolean isRejector(AgentSession agentSession) {
        Date rejectionTime = rejectionTimes.get(agentSession.getJID().toBareJID());
        boolean rejector = false;
        if (rejectionTime != null) {
            if (rejectionTime.getTime() > System.currentTimeMillis() - rejectionTimeout) {
                rejector = true;
            }
            else {
                rejectionTimes.remove(agentSession.getJID().toBareJID());
            }
        }
        return rejector;
    }

    public List<AgentSession> getAcceptedSessions() {
        return Collections.unmodifiableList(acceptedSessions);
    }

    public Collection<String> getRejections() {
        return rejections;
    }

    public Date getOfferTime() {
        return offerTime;
    }

    public long getTimeout() {
        return timeout;
    }

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public void invite(AgentSession agentSession) {
        if (acceptedSessions.contains(agentSession) && request != null) {
            // Ask the workgroup to send invitations to the agent and to the user that made the
            // request. The Workgroup will create a MUC room and send invitations to the agent and
            // the user.
            request.offerAccepted(agentSession);
            updateUserSession(ROUTED);
            invitationSent = true;
        }
    }

    public void waitForResolution() {
        long timeoutTime = offerTime.getTime() + timeout;
        while (timeoutTime > System.currentTimeMillis() && !isAccepted() && !pendingSessions.isEmpty()) {
            try {
                Thread.sleep(500); // half second polling
            }
            catch (InterruptedException e) {
                // do nothing
            }
        }
        if (!isAccepted()) {
            try {
                for (AgentSession session : pendingSessions) {
                    request.sendRevoke(session, queue);
                    reject(session);
                }
            }
            catch (Exception e) {
                // Ignore
            }
        }
    }

    public void cancel() {
        cancelled = true;

        // Handle when customer cancels.
        if (!pendingSessions.isEmpty() || !acceptedSessions.isEmpty()) {
            for (AgentSession session : pendingSessions) {
                request.sendRevoke(session, queue);
            }
            for (AgentSession session : acceptedSessions) {
                request.sendRevoke(session, queue);
            }
            pendingSessions.clear();
            acceptedSessions.clear();
            updateUserSession(USER_CANCELLED);
        }
        else {
            updateUserSession(ROUTE_EXPIRED);
        }
    }

    public void addPendingSession(AgentSession agentSession) {
        pendingSessions.add(agentSession);
        // reset the Offer time
        offerTime = new Date();
    }

    /**
     * Returns true if the offer is still outstanding. An offer is considered outstanding if it has
     * not been cancelled and an invitation was not yet sent to an agent.
     *
     * @return true if the offer is still outstanding.
     */
    public boolean isOutstanding() {
        return !cancelled && !invitationSent;
    }

    @Override
    public int hashCode() {
        return request.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        boolean eq = false;
        if (obj instanceof Offer){
            Offer otherOffer = (Offer)obj;
            eq = request.equals(otherOffer.getRequest());
        }
        return eq;
    }

    /**
     * Updates the database tables with new session state, waitTime and metadata.
     *
     * @param state the state of this session.
     */
    private void updateUserSession(int state) {
        // Update the current session.
        request.updateSession(state, offerTime.getTime());
    }
}
