/*
 * Copyright (C) 2005-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.openfire.privacy;

import java.io.StringReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;

import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jivesoftware.database.DbConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provider for the privacy lists system. Privacy lists are read and written
 * from the <tt>ofPrivacyList</tt> database table.
 *
 * @author Gaston Dombiak
 */
public class PrivacyListProvider {

    private static final PrivacyListProvider instance = new PrivacyListProvider();
    
    private static final Logger Log = LoggerFactory.getLogger(PrivacyListProvider.class);

    private static final String PRIVACY_LIST_COUNT =
            "SELECT count(*) from ofPrivacyList";
    private static final String LOAD_LIST_NAMES =
            "SELECT name, isDefault FROM ofPrivacyList WHERE username=?";
    private static final String LOAD_PRIVACY_LIST =
            "SELECT isDefault, list FROM ofPrivacyList WHERE username=? AND name=?";
    private static final String LOAD_DEFAULT_PRIVACY_LIST =
            "SELECT name, list FROM ofPrivacyList WHERE username=? AND isDefault=1";
    private static final String DELETE_PRIVACY_LIST =
            "DELETE FROM ofPrivacyList WHERE username=? AND name=?";
    private static final String DELETE_PRIVACY_LISTS =
            "DELETE FROM ofPrivacyList WHERE username=?";
    private static final String UPDATE_PRIVACY_LIST =
            "UPDATE ofPrivacyList SET isDefault=?, list=? WHERE username=? AND name=?";
    private static final String INSERT_PRIVACY_LIST =
            "INSERT INTO ofPrivacyList (username, name, isDefault, list) VALUES (?, ?, ?, ?)";

    private static final int POOL_SIZE = 50;
    /**
     * Pool of SAX Readers. SAXReader is not thread safe so we need to have a pool of readers.
     */
    private BlockingQueue<SAXReader> xmlReaders = new LinkedBlockingQueue<>(POOL_SIZE);

    /**
     * Boolean used to optimize getters when the database is empty
     */
    private AtomicBoolean databaseContainsPrivacyLists;

    /**
     * Returns the unique instance of this class.
     *
     * @return the unique instance of this class.
     */
    public static PrivacyListProvider getInstance() {
        return instance;
    }
    
    private PrivacyListProvider() {
        super();
        // Initialize the pool of sax readers
        for (int i=0; i<POOL_SIZE; i++) {
            SAXReader xmlReader = new SAXReader();
            xmlReader.setEncoding("UTF-8");
            xmlReaders.add(xmlReader);
        }

        // Checks if the PrivacyLists database is empty. 
        // In that case, we can optimize away many database calls. 
        databaseContainsPrivacyLists = new AtomicBoolean(false);
        loadDatabaseContainsPrivacyLists();
    }

    /**
     * Returns the names of the existing privacy lists indicating which one is the
     * default privacy list associated to a user.
     *
     * @param username the username of the user to get his privacy lists names.
     * @return the names of the existing privacy lists with a default flag.
     */
    public Map<String, Boolean> getPrivacyLists(String username) {
        // If there are no privacy lists stored, this method is a no-op.
        if (!databaseContainsPrivacyLists.get()) {
            return Collections.emptyMap();
        }

        Map<String, Boolean> names = new HashMap<>();
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(LOAD_LIST_NAMES);
            pstmt.setString(1, username);
            rs = pstmt.executeQuery();
            while (rs.next()) {
                names.put(rs.getString(1), rs.getInt(2) == 1);
            }
        }
        catch (Exception e) {
            Log.error("Error loading names of privacy lists for username: " + username, e);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        return names;
    }

    /**
     * Loads the requested privacy list from the database. Returns <tt>null</tt> if a list
     * with the specified name does not exist.
     *
     * @param username the username of the user to get his privacy list.
     * @param listName name of the list to load.
     * @return the privacy list with the specified name or <tt>null</tt> if a list
     *         with the specified name does not exist.
     */
    public PrivacyList loadPrivacyList(String username, String listName) {
        // If there are no privacy lists stored, this method is a no-op.
        if (!databaseContainsPrivacyLists.get()) {
            return null;
        }

        boolean isDefault = false;
        String listValue = null;

        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(LOAD_PRIVACY_LIST);
            pstmt.setString(1, username);
            pstmt.setString(2, listName);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                isDefault = rs.getInt(1) == 1;
                listValue = rs.getString(2);
            }
            else {
                return null;
            }
        }
        catch (Exception e) {
            Log.error("Error loading privacy list: " + listName + " of username: " + username, e);
            return null;
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }

        PrivacyList privacyList = null;
        SAXReader xmlReader = null;
        try {
            // Get a sax reader from the pool
            xmlReader = xmlReaders.take();
            Element listElement = xmlReader.read(new StringReader(listValue)).getRootElement();
            privacyList = new PrivacyList(username, listName, isDefault, listElement);
        }
        catch (Exception e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            // Return the sax reader to the pool
            if (xmlReader != null) {
                xmlReaders.add(xmlReader);
            }
        }


        return privacyList;
    }

    /**
     * Loads the default privacy list of a given user from the database. Returns <tt>null</tt>
     * if no list was found.
     *
     * @param username the username of the user to get his default privacy list.
     * @return the default privacy list or <tt>null</tt> if no list was found.
     */
    public PrivacyList loadDefaultPrivacyList(String username) {
        // If there are no privacy lists stored, this method is a no-op.
        if (!databaseContainsPrivacyLists.get()) {
            return null;
        }

        String listName = null;
        String listValue = null;

        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(LOAD_DEFAULT_PRIVACY_LIST);
            pstmt.setString(1, username);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                listName = rs.getString(1);
                listValue = rs.getString(2);
            }
            else {
                return null;
            }
        }
        catch (Exception e) {
            Log.error("Error loading default privacy list of username: " + username, e);
            return null;
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }

        PrivacyList privacyList = null;
        SAXReader xmlReader = null;
        try {
            // Get a sax reader from the pool
            xmlReader = xmlReaders.take();
            Element listElement = xmlReader.read(new StringReader(listValue)).getRootElement();
            privacyList = new PrivacyList(username, listName, true, listElement);
        }
        catch (Exception e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            // Return the sax reader to the pool
            if (xmlReader != null) {
                xmlReaders.add(xmlReader);
            }
        }

        return privacyList;
    }

    /**
     * Creates and saves the new privacy list to the database.
     *
     * @param username the username of the user that created a new privacy list.
     * @param list the PrivacyList to save.
     */
    public void createPrivacyList(String username, PrivacyList list) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(INSERT_PRIVACY_LIST);
            pstmt.setString(1, username);
            pstmt.setString(2, list.getName());
            pstmt.setInt(3, (list.isDefault() ? 1 : 0));
            pstmt.setString(4, list.asElement().asXML());
            pstmt.executeUpdate();
        }
        catch (Exception e) {
            Log.error("Error adding privacy list: " + list.getName() + " of username: " + username,
                    e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        databaseContainsPrivacyLists.set(true);
    }

    /**
     * Updated the existing privacy list in the database.
     *
     * @param username the username of the user that updated a privacy list.
     * @param list the PrivacyList to update in the database.
     */
    public void updatePrivacyList(String username, PrivacyList list) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(UPDATE_PRIVACY_LIST);
            pstmt.setInt(1, (list.isDefault() ? 1 : 0));
            pstmt.setString(2, list.asElement().asXML());
            pstmt.setString(3, username);
            pstmt.setString(4, list.getName());
            pstmt.executeUpdate();
        }
        catch (Exception e) {
            Log.error("Error updating privacy list: " + list.getName() + " of username: " +
                    username, e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        databaseContainsPrivacyLists.set(true);
    }

    /**
     * Deletes an existing privacy list from the database.
     *
     * @param username the username of the user that deleted a privacy list.
     * @param listName the name of the PrivacyList to delete.
     */
    public void deletePrivacyList(String username, String listName) {
        // If there are no privacy lists stored, this method is a no-op.
        if (!databaseContainsPrivacyLists.get()) {
            return;
        }
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(DELETE_PRIVACY_LIST);
            pstmt.setString(1, username);
            pstmt.setString(2, listName);
            pstmt.executeUpdate();
        }
        catch (Exception e) {
            Log.error("Error deleting privacy list: " + listName + " of username: " + username, e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        databaseContainsPrivacyLists.set(true);
    }

    /**
     * Deletes all existing privacy list from the database for the given user.
     *
     * @param username the username of the user whose privacy lists are going to be deleted.
     */
    public void deletePrivacyLists(String username) {
        // If there are no privacy lists stored, this method is a no-op.
        if (!databaseContainsPrivacyLists.get()) {
            return;
        }
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(DELETE_PRIVACY_LISTS);
            pstmt.setString(1, username);
            pstmt.executeUpdate();
        }
        catch (Exception e) {
            Log.error("Error deleting privacy lists of username: " + username, e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        databaseContainsPrivacyLists.set(true);
    }

    /**
     * Loads the total number of privacy lists stored in the database to know if we must use them.
     */
    private void loadDatabaseContainsPrivacyLists() {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(PRIVACY_LIST_COUNT);
            rs = pstmt.executeQuery();
            rs.next();
            databaseContainsPrivacyLists.set(rs.getInt(1) != 0);
        }
        catch (Exception e) {
            Log.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
    }
}
