/*
 * 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.routing;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.xmpp.workgroup.RequestQueue;
import org.jivesoftware.xmpp.workgroup.Workgroup;
import org.jivesoftware.xmpp.workgroup.request.UserRequest;
import org.jivesoftware.xmpp.workgroup.spi.routers.WordMatchRouter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provides a registration and event processing for all <code>RequestRouter</code>s.
 *
 * @author Derek DeMoro
 */
public final class RoutingManager {

    private static final Logger Log = LoggerFactory.getLogger(RoutingManager.class);
    
    private static final String ADD_ROUTING_RULE =
            "INSERT INTO fpRouteRule (workgroupID, queueID, rulePosition, query) VALUES (?,?,?,?)";
    private static final String DELETE_ROUTING_RULE =
            "DELETE FROM fpRouteRule WHERE workgroupID=? AND rulePosition=?";
    private static final String LOAD_RULES =
            "SELECT queueID, rulePosition, query FROM fpRouteRule WHERE workgroupID=?";
    private static final String UPDATE_RULE_POSITION =
            "UPDATE fpRouteRule SET rulePosition=? WHERE workgroupID=? AND rulePosition=?";

    private static RoutingManager singleton = new RoutingManager();

    /**
     * Returns the singleton instance of <CODE>RoutingManager</CODE>,
     * creating it if necessary.
     * <p/>
     *
     * @return the singleton instance of <Code>RoutingManager</CODE>
     */
    public static RoutingManager getInstance() {
        return singleton;
    }

    public static void shutdown() {
        singleton = null;
    }

    private RoutingManager() {

    }

    /**
     * Returns the best {@link RequestQueue} in the specified {@link Workgroup} that could
     * handle the specified {@link UserRequest}.
     *
     * @param workgroup the workgroup where a queue will be searched.
     * @param request the user request to be handled.
     * @return the best RequestQueue in the specified Workgroup.
     */
    public RequestQueue getBestQueue(Workgroup workgroup, UserRequest request) {
        WordMatchRouter router = new WordMatchRouter();
        for (RoutingRule rule : getRoutingRules(workgroup)) {
            String query = rule.getQuery();

            boolean handled = router.checkForHits(request.getMetaData(), query);
            if (handled) {
                // Retrieve queue and route to it.
                try {
                    return workgroup.getRequestQueue(rule.getQueueID());
                }
                catch (NotFoundException e) {
                    Log.error(e.getMessage(), e);
                }
            }
        }

        List<RequestQueue> availableRequestQueues = new ArrayList<RequestQueue>();
        // Route to best queue based on availability.
        for (RequestQueue requestQueue : workgroup.getRequestQueues()) {
            // Skip queues that do not have agents at the moment
            if (requestQueue != null && requestQueue.isOpened()) {
                availableRequestQueues.add(requestQueue);
            }
        }

        Collections.sort(availableRequestQueues, queueComparator);
        return availableRequestQueues.get(0);
    }

    /**
     * Routes a request to all registers globalRouters within the Live Assistant Server.
     *
     * @param workgroup the workgroup the request came in on.
     * @param request   the actual request.
     */
    public void routeRequest(Workgroup workgroup, UserRequest request) {
        getBestQueue(workgroup, request).addRequest(request);
    }

    /**
     * Adds a new Routing Rule to the database.
     *
     * @param workgroup the <code>Workgroup</code> the routing rule belongs to.
     * @param queueID the id of the queue.
     * @param position the position of the routing rule.
     * @param query the lucense query
     */
    public void addRoutingRule(Workgroup workgroup, long queueID, int position, String query) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(ADD_ROUTING_RULE);

            pstmt.setLong(1, workgroup.getID());
            pstmt.setLong(2, queueID);
            pstmt.setInt(3, position);
            pstmt.setString(4, query);

            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
    }

    /**
     * Updates a Routing rule to a new position.
     *
     * @param workgroup the workgroup the rule belongs to.
     * @param position the current position.
     * @param newPosition the new position.
     */
    public void updateRoutingRule(Workgroup workgroup, int position, int newPosition) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(UPDATE_RULE_POSITION);

            pstmt.setInt(1, newPosition);
            pstmt.setLong(2, workgroup.getID());
            pstmt.setInt(3, position);

            pstmt.execute();
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
    }

    /**
     * Removes a RoutingRule from the database.
     *
     * @param workgroup the workgroup the routing rule belongs to.
     * @param position the position of the routing rule.
     */
    public void removeRoutingRule(Workgroup workgroup, int position) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(DELETE_ROUTING_RULE);

            pstmt.setLong(1, workgroup.getID());
            pstmt.setInt(2, position);

            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
    }

    /**
     * Returns all RoutingRules belong to the given workgroup.
     *
     * @param workgroup the workgroup.
     * @return a Collection of all RoutingRules sorted by position.
     */
    public Collection<RoutingRule> getRoutingRules(Workgroup workgroup) {
        final List<RoutingRule> rules = new ArrayList<RoutingRule>();

        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(LOAD_RULES);
            pstmt.setLong(1, workgroup.getID());
            rs = pstmt.executeQuery();
            while (rs.next()) {
                long queueID = rs.getLong("queueID");
                int position = rs.getInt("rulePosition");
                String query = rs.getString("query");

                RoutingRule rule = new RoutingRule(queueID, position, query);
                rules.add(rule);
            }
        }
        catch (Exception ex) {
            Log.error(ex.getMessage(), ex);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }

        // Sort by position.
        Collections.sort(rules, positionComparator);

        return rules;
    }

    private final Comparator<RoutingRule> positionComparator = new Comparator<RoutingRule>() {
        public int compare(RoutingRule rule1, RoutingRule rule2) {
            int int1 = rule1.getPosition();
            int int2 = rule2.getPosition();

            if (int1 == int2) {
                return 0;
            }

            if (int1 > int2) {
                return 1;
            }

            if (int1 < int2) {
                return -1;
            }

            return 0;
        }
    };

    private final Comparator<RequestQueue> queueComparator = new Comparator<RequestQueue>() {
        public int compare(RequestQueue queue1, RequestQueue queue2) {
            int int1 = queue1.getTotalRequestCount();
            int int2 = queue2.getTotalRequestCount();

            if (int1 == int2) {
                return 0;
            }

            if (int1 > int2) {
                return 1;
            }

            if (int1 < int2) {
                return -1;
            }

            return 0;
        }
    };
}
