

package org.jivesoftware.openfire.group;

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.List;

import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;

/**
 * Database implementation of the GroupManager interface.
 *
 * @author Matt Tucker
 */
public class DefaultGroupProvider extends AbstractGroupProvider {

    private static final Logger Log = LoggerFactory.getLogger(DefaultGroupProvider.class);

    private static final String INSERT_GROUP =
        "INSERT INTO ofGroup (groupName, description) VALUES (?, ?)";
    private static final String SAVE_GROUP =
        "UPDATE ofGroup SET description=? WHERE groupName=?";
    private static final String SET_GROUP_NAME_1 =
        "UPDATE ofGroup SET groupName=? WHERE groupName=?";
    private static final String SET_GROUP_NAME_2 =
        "UPDATE ofGroupProp SET groupName=? WHERE groupName=?";
    private static final String SET_GROUP_NAME_3 =
        "UPDATE ofGroupUser SET groupName=? WHERE groupName=?";
    private static final String DELETE_GROUP_USERS =
        "DELETE FROM ofGroupUser WHERE groupName=?";
    private static final String DELETE_PROPERTIES =
        "DELETE FROM ofGroupProp WHERE groupName=?";
    private static final String DELETE_GROUP =
        "DELETE FROM ofGroup WHERE groupName=?";
    private static final String GROUP_COUNT = "SELECT count(*) FROM ofGroup";
     private static final String LOAD_ADMINS =
        "SELECT username FROM ofGroupUser WHERE administrator=1 AND groupName=? ORDER BY username";
    private static final String LOAD_MEMBERS =
        "SELECT username FROM ofGroupUser WHERE administrator=0 AND groupName=? ORDER BY username";
    private static final String LOAD_GROUP =
        "SELECT description FROM ofGroup WHERE groupName=?";
    private static final String REMOVE_USER =
        "DELETE FROM ofGroupUser WHERE groupName=? AND username=?";
    private static final String ADD_USER =
        "INSERT INTO ofGroupUser (groupName, username, administrator) VALUES (?, ?, ?)";
    private static final String UPDATE_USER =
        "UPDATE ofGroupUser SET administrator=? WHERE groupName=? AND username=?";
    private static final String USER_GROUPS =
        "SELECT groupName FROM ofGroupUser WHERE username=?";
    private static final String ALL_GROUPS = "SELECT groupName FROM ofGroup ORDER BY groupName";
    private static final String SEARCH_GROUP_NAME = "SELECT groupName FROM ofGroup WHERE groupName LIKE ? ORDER BY groupName";

    private XMPPServer server = XMPPServer.getInstance();

    @Override
    public Group createGroup(String name) throws GroupNameInvalidException, GroupAlreadyExistsException {
        super.createGroup(name);

        if(name.trim().isEmpty()){
            throw new GroupNameInvalidException("Group name cannot be empty");
        }
        try {
            this.getGroup(name);
            throw new GroupAlreadyExistsException("A group with the same name already exists");
        } catch (GroupNotFoundException e) {
            //Expected, do nothing
        }
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(INSERT_GROUP);
            pstmt.setString(1, name);
            pstmt.setString(2, "");
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        Collection<JID> members = getMembers(name, false);
        Collection<JID> administrators = getMembers(name, true);
        return new Group(name, "", members, administrators);
    }

    @Override
    public Group getGroup(String name) throws GroupNotFoundException {
        String description = null;

        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(LOAD_GROUP);
            pstmt.setString(1, name);
            rs = pstmt.executeQuery();
            if (!rs.next()) {
                throw new GroupNotFoundException("Group with name "
                    + name + " not found.");
            }
            description = rs.getString(1);
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        Collection<JID> members = getMembers(name, false);
        Collection<JID> administrators = getMembers(name, true);
        return new Group(name, description, members, administrators);
    }

    @Override
    public void setDescription(String name, String description) throws GroupNotFoundException {
        super.setDescription(name, description);

        try {
            this.getGroup(name);
        } catch (GroupNotFoundException e) {
            throw new GroupNotFoundException("A group with this name cannot be found");
        }

        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(SAVE_GROUP);
            pstmt.setString(1, description);
            pstmt.setString(2, name);
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
            throw new GroupNotFoundException(e); //We wrap using this, even though it probably isn't...
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
    }

    @Override
    public void setName(String oldName, String newName) throws GroupAlreadyExistsException, GroupNameInvalidException, GroupNotFoundException {
        super.setName(oldName, newName);

        if(newName.trim().isEmpty()){
            throw new GroupNameInvalidException("Group name cannot be empty");
        }

        try {
            this.getGroup(oldName);
        } catch (GroupNotFoundException e) {
            throw new GroupNotFoundException("A group with this name cannot be found");
        }

        try {
            this.getGroup(newName);
            throw new GroupAlreadyExistsException("A group with the same name already exists");
        } catch (GroupNotFoundException e) {
            //Expected, do nothing
        }

        Connection con = null;
        PreparedStatement pstmt = null;
        boolean abortTransaction = false;
        try {
            con = DbConnectionManager.getTransactionConnection();
            pstmt = con.prepareStatement(SET_GROUP_NAME_1);
            pstmt.setString(1, newName);
            pstmt.setString(2, oldName);
            pstmt.executeUpdate();
            DbConnectionManager.fastcloseStmt(pstmt);
            
            pstmt = con.prepareStatement(SET_GROUP_NAME_2);
            pstmt.setString(1, newName);
            pstmt.setString(2, oldName);
            pstmt.executeUpdate();
            DbConnectionManager.fastcloseStmt(pstmt);
            
            pstmt = con.prepareStatement(SET_GROUP_NAME_3);
            pstmt.setString(1, newName);
            pstmt.setString(2, oldName);
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
            abortTransaction = true;
        }
        finally {
            DbConnectionManager.closeStatement(pstmt);
            DbConnectionManager.closeTransactionConnection(con, abortTransaction);
        }
    }

    @Override
    public void deleteGroup(String groupName) throws GroupNotFoundException {
        super.deleteGroup(groupName);

        Connection con = null;
        PreparedStatement pstmt = null;
        boolean abortTransaction = false;
        try {
            con = DbConnectionManager.getTransactionConnection();
            // Remove all users in the group.
            pstmt = con.prepareStatement(DELETE_GROUP_USERS);
            pstmt.setString(1, groupName);
            pstmt.executeUpdate();
            DbConnectionManager.fastcloseStmt(pstmt);
            
            // Remove all properties of the group.
            pstmt = con.prepareStatement(DELETE_PROPERTIES);
            pstmt.setString(1, groupName);
            pstmt.executeUpdate();
            DbConnectionManager.fastcloseStmt(pstmt);
            
            // Remove the group entry.
            pstmt = con.prepareStatement(DELETE_GROUP);
            pstmt.setString(1, groupName);
            int groupsDeleted = pstmt.executeUpdate();
            if (groupsDeleted == 0){
                throw new GroupNotFoundException("Did not find group to delete");
            }
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
            abortTransaction = true;
        }
        finally {
            DbConnectionManager.closeStatement(pstmt);
            DbConnectionManager.closeTransactionConnection(con, abortTransaction);
        }
    }

    @Override
    public int getGroupCount() {
        int count = 0;
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(GROUP_COUNT);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                count = rs.getInt(1);
            }
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        return count;
    }

    @Override
    public Collection<String> getGroupNames() {
        List<String> groupNames = new ArrayList<>();
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(ALL_GROUPS);
            rs = pstmt.executeQuery();
            while (rs.next()) {
                groupNames.add(rs.getString(1));
            }
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);       }
        return groupNames;
    }

    @Override
    public Collection<String> getGroupNames(int startIndex, int numResults) {
        List<String> groupNames = new ArrayList<>();
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = DbConnectionManager.createScrollablePreparedStatement(con, ALL_GROUPS);
            rs = pstmt.executeQuery();
            DbConnectionManager.scrollResultSet(rs, startIndex);
            int count = 0;
            while (rs.next() && count < numResults) {
                groupNames.add(rs.getString(1));
                count++;
            }
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        return groupNames;
    }

    @Override
    public Collection<String> getGroupNames(JID user) {
        List<String> groupNames = new ArrayList<>();
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(USER_GROUPS);
            pstmt.setString(1, server.isLocal(user) ? user.getNode() : user.toString());
            rs = pstmt.executeQuery();
            while (rs.next()) {
                groupNames.add(rs.getString(1));
            }
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        return groupNames;
    }

    @Override
    public void addMember(String groupName, JID user, boolean administrator) throws GroupNotFoundException {
        super.addMember(groupName, user, administrator);

        try {
            this.getGroup(groupName);
        } catch (GroupNotFoundException e) {
            throw new GroupNotFoundException("A group with this name cannot be found");
        }

        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(ADD_USER);
            pstmt.setString(1, groupName);
            pstmt.setString(2, server.isLocal(user) ? user.getNode() : user.asBareJID().toString());
            pstmt.setInt(3, administrator ? 1 : 0);
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
    }

    @Override
    public void updateMember(String groupName, JID user, boolean administrator) throws GroupNotFoundException {
        super.updateMember(groupName, user, administrator);

        try {
            this.getGroup(groupName);
        } catch (GroupNotFoundException e) {
            throw new GroupNotFoundException("A group with this name cannot be found");
        }

        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(UPDATE_USER);
            pstmt.setInt(1, administrator ? 1 : 0);
            pstmt.setString(2, groupName);
            pstmt.setString(3, server.isLocal(user) ? user.getNode() : user.toString());
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
    }

    @Override
    public void deleteMember(String groupName, JID user) {
        super.deleteMember(groupName, user);

        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(REMOVE_USER);
            pstmt.setString(1, groupName);
            pstmt.setString(2, server.isLocal(user) ? user.getNode() : user.toString());
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
    }

    @Override
    public boolean isReadOnly() {
        return false;
    }

    @Override
    public Collection<String> search(String query) {
        return search(query, 0, Integer.MAX_VALUE);
    }

    @Override
    public Collection<String> search(String query, int startIndex, int numResults) {
        if (query == null || "".equals(query)) {
            return Collections.emptyList();
        }
        // SQL LIKE queries don't map directly into a keyword/wildcard search like we want.
        // Therefore, we do a best approximiation by replacing '*' with '%' and then
        // surrounding the whole query with two '%'. This will return more data than desired,
        // but is better than returning less data than desired.
        query = "%" + query.replace('*', '%') + "%";
        if (query.endsWith("%%")) {
            query = query.substring(0, query.length()-1);
        }

        List<String> groupNames = new ArrayList<>();
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            if ((startIndex==0) && (numResults==Integer.MAX_VALUE))
            {
               pstmt = con.prepareStatement(SEARCH_GROUP_NAME);
               pstmt.setString(1, query);
               rs = pstmt.executeQuery();
               while (rs.next()) {
                   groupNames.add(rs.getString(1));
               }
            } else {
               pstmt = DbConnectionManager.createScrollablePreparedStatement(con, SEARCH_GROUP_NAME);
               DbConnectionManager.limitRowsAndFetchSize(pstmt, startIndex, numResults);
               pstmt.setString(1, query);
               rs = pstmt.executeQuery();
               // Scroll to the start index.
               DbConnectionManager.scrollResultSet(rs, startIndex);
               int count = 0;
               while (rs.next() && count < numResults) {
                   groupNames.add(rs.getString(1));
                   count++;
               }	
            }
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        return groupNames;
    }

    @Override
    public boolean isSearchSupported() {
        return true;
    }

    @Override
    public boolean isSharingSupported() {
        return true;
    }

    private Collection<JID> getMembers(String groupName, boolean adminsOnly) {
        List<JID> members = new ArrayList<>();
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            if (adminsOnly) {
                pstmt = con.prepareStatement(LOAD_ADMINS);
            }
            else {
                pstmt = con.prepareStatement(LOAD_MEMBERS);
            }
            pstmt.setString(1, groupName);
            rs = pstmt.executeQuery();
            while (rs.next()) {
                String user = rs.getString(1);
                JID userJID = null;
                if (user.indexOf('@') == -1) {
                    // Create JID of local user if JID does not match a component's JID
                    if (!server.matchesComponent(userJID)) {
                        userJID = server.createJID(user, null);
                    }
                }
                else {
                    userJID = new JID(user);
                }
                members.add(userJID);
            }
        }
        catch (SQLException e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        return members;
    }

}
