package de.uka.ipd.consensus.impl;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import javax.swing.event.EventListenerList;

import de.uka.ipd.consensus.foundation.AbstractConsensusFoundationModule;
import de.uka.ipd.consensus.foundation.ConsensusFoundation;
import de.uka.ipd.consensus.foundation.ConsensusFoundationManager;
import de.uka.ipd.consensus.foundation.logging.LoggingManager;
import de.uka.ipd.consensus.foundation.schema.User;
import de.uka.ipd.consensus.foundation.scoring.IncentiveManager;
import de.uka.ipd.consensus.foundation.user.UserManagerInvalidUsernameException;
import de.uka.ipd.consensus.foundation.user.UserEvent;
import de.uka.ipd.consensus.foundation.user.UserListener;
import de.uka.ipd.consensus.foundation.user.UserManager;
import de.uka.ipd.consensus.foundation.user.UserManagerException;
import de.uka.ipd.consensus.foundation.user.UserManagerInvalidPasswordException;
import de.uka.ipd.consensus.foundation.user.UserRole;

/**
 * 
 * TODO
 * @author Thomas Much
 * @version 2006-06-14
 */
public class UserManagerImpl extends AbstractConsensusFoundationModule implements UserManager {

	public static final String USERDATA_FIRSTNAME_KEY = "firstname";
	public static final String USERDATA_LASTNAME_KEY  = "lastname";
	public static final String USERDATA_EMAIL_KEY     = "email";
	public static final String USERDATA_COMMENT_KEY   = "comment";

	public static final String ROLE_SUPERADMIN = "superadmin";
	public static final String ROLE_ADMIN      = "admin";


	private static final String USERS_TABLE      = "CFUSERS";
	private static final String USERS_DATASOURCE = "jdbc/datenquelle";

	private String            superAdmin;
	private DataSource        pool;
	private EventListenerList listenerList = new EventListenerList();
	private Map               roles = new HashMap();

	private boolean running = false;

	

	public void init(Properties config, LoggingManager logging) throws Exception {
		
		running = true;

		superAdmin = config.getProperty( ConsensusFoundation.CF_SUPERADMIN_KEY );

		Context init = new InitialContext( config );
		Context env  = (Context)init.lookup("java:comp/env");
		
		String source = config.getProperty("cfimpl.userdatasource");
		
		if ((source == null) || (source.length() == 0)) {
			source = USERS_DATASOURCE;
		}

		pool = (DataSource)env.lookup(source);
		
		if (Boolean.valueOf(config.getProperty("cfimpl.initdatabase")).booleanValue()) {
			initDatabase();
		}
		
		createRole( ROLE_SUPERADMIN, "May assign and revoke the (super)admin roles to and from other users." );
		createRole( ROLE_ADMIN, "May create root topics etc. even if the schema is fixed and export the ontology." );

		logging.debug("UserManager init done.");
	}


	public void initCompleted(Properties config, ConsensusFoundationManager cfm) throws Exception {
		super.initCompleted(config, cfm);
		cfm.getLoggingManager().debug("UserManager initCompleted done.");
	}

	
	public void shutdown() {
		if (running) {
			running = false;
			getLoggingManager().debug("UserManager shutdown...");
		}
	}


	public User login(String username, String password) throws UserManagerException {
		UserImpl user = (UserImpl)this.getUserByUsername( username );

		if ((user == null) || (user.getPasswordHash() != password.hashCode())) {
			throw new UserManagerException("password incorrect");
		}
		else {
			this.loginUser(user);
			return user;
		}
	}


	public void logout(User user) throws UserManagerException {
		UserImpl impl = (UserImpl)user;

		if (!impl.isLoggedOn()) {
			throw new UserManagerException(user + " not logged on");
		}
		
		getLoggingManager().info("logout " + user + " " + new Date());

		fireUserLoggedOut(user);
		
		impl.setLoggedOn(false);
	}
	
	
	private boolean isValidXMLID(String id) {
		// see <http://www.w3.org/TR/2000/REC-xml-20001006#id> (but we're accepting only a subset!)
		if ((id == null) || (id.length() == 0)) return false;

		char ch = id.charAt(0);

		if ((ch == '_') || (ch == ':') || Character.isLetter(ch)) {

			for (int i = 1; i < id.length(); i++) {
				ch = id.charAt( i );

				if ((ch != '_')
						&& (ch != ':')
						&& (ch != '.')
						&& (ch != '-')
						&& (!Character.isLetterOrDigit( ch ))) {
					return false;
				}
			}
			
			return true;
		}

		return false;
	}


	public User createUser(String username, String password, Map data) throws UserManagerException {

		if (!isValidXMLID( username )) {
			throw new UserManagerInvalidUsernameException();
		}

		String firstname = null;
		String lastname  = null;
		String email     = null;
		String comment   = null;
		String roles     = null;
		
		if (data != null) {
			firstname = (String)data.get( USERDATA_FIRSTNAME_KEY );
			lastname  = (String)data.get( USERDATA_LASTNAME_KEY );
			email     = (String)data.get( USERDATA_EMAIL_KEY );
			comment   = (String)data.get( USERDATA_COMMENT_KEY );
		}

		UserImpl   user = null;
		Connection con  = null;

		try {
			con = pool.getConnection();

			con.setReadOnly(true);

			PreparedStatement ps = con.prepareStatement("SELECT BENUTZERNAME FROM " + USERS_TABLE + " WHERE BENUTZERNAME=?");
			
			ps.setString(1,username);
			
			ResultSet rs = ps.executeQuery();
			
			boolean schonVorhanden = rs.next();

			rs.close();
			ps.close();
		
			
			if (!schonVorhanden)
			{
				double initScore = 0.0;
				
				IncentiveManager manager = this.getIncentiveManager();
				
				if (manager != null) {
					initScore = manager.getInitialUserScore();
				}
				
				int passwordHash = password.hashCode();

				con.setReadOnly(false);
				
				ps = con.prepareStatement("INSERT INTO " + USERS_TABLE + " (BENUTZERNAME,PASSWORT,VORNAME,NACHNAME,EMAIL,SCORE,COMMENT,ROLES) VALUES (?,?,?,?,?,?,?,?)");
				
				ps.setString(1,username);
				ps.setInt(2,passwordHash);
				ps.setString(3,firstname);
				ps.setString(4,lastname);
				ps.setString(5,email);
				ps.setDouble(6,initScore);
				ps.setString(7,comment);
				ps.setString(8,roles);
				
				ps.execute();
				
				ps.close();
				
				user = UserImpl.getInstance(this,username,passwordHash,firstname,lastname,email,initScore,comment,roles);

				fireUserCreated(user);
				this.loginUser(user);
			}
		}
		catch (Exception e) {
			throw new UserManagerException(e);
		}
		finally {
			if (con != null) {
				try {
					con.close();
				}
				catch (Exception e) {
					throw new UserManagerException(e);
				}
			}
		}

		if (user == null) {
			throw new UserManagerInvalidUsernameException();
		}
		else {
			return user;
		}
	}


	public void changeUser(User user, String oldPassword, String newPassword, Map data) throws UserManagerException {
		// TODO: changer als ersten Parameter übergeben? muss entweder ==user sein oder aber superadmin
		
		UserImpl ui = (UserImpl)user;

		int newPasswordHash = ui.getPasswordHash();

		if ((newPassword != null) && (newPassword.length() > 0)) {
			if ((oldPassword == null) || (oldPassword.hashCode() != ui.getPasswordHash())) {
				throw new UserManagerInvalidPasswordException();
			}
			newPasswordHash = newPassword.hashCode();
		}
		
		String firstname = (String)data.get( USERDATA_FIRSTNAME_KEY );
		String lastname  = (String)data.get( USERDATA_LASTNAME_KEY );
		String email     = (String)data.get( USERDATA_EMAIL_KEY );
		String comment   = (String)data.get( USERDATA_COMMENT_KEY );
		
		Connection con  = null;
		
		try {
			con = pool.getConnection();
			
			con.setReadOnly(false);
	
			PreparedStatement ps = con.prepareStatement("UPDATE " + USERS_TABLE + " SET PASSWORT=?,VORNAME=?,NACHNAME=?,EMAIL=?,COMMENT=? WHERE BENUTZERNAME=?");
			
			ps.setInt(1,newPasswordHash);
			ps.setString(2,firstname);
			ps.setString(3,lastname);
			ps.setString(4,email);
			ps.setString(5,comment);
			ps.setString(6,user.getUsername());
			
			ps.execute();
			
			ps.close();
		}
		catch (Exception e) {
			throw new UserManagerException(e);
		}
		finally {
			if (con != null) {
				try {
					con.close();
				}
				catch (Exception e) {
					throw new UserManagerException(e);
				}
			}
		}

		ui.change( newPasswordHash, firstname, lastname, email, comment );
	}


	public void deleteUser(User user, String password) throws UserManagerException {
		// TODO: wie löscht der Superadmin einen User?
		// TODO: deleter als ersten Parameter übergeben? muss entweder ==user sein oder aber superadmin
		
		if ((password == null) || (password.hashCode() != ((UserImpl)user).getPasswordHash())) {
			throw new UserManagerInvalidPasswordException();
		}

		Connection con  = null;

		try {
			con = pool.getConnection();
			
			con.setReadOnly(false);

			PreparedStatement ps = con.prepareStatement("DELETE FROM " + USERS_TABLE + " WHERE BENUTZERNAME=?");
	
			ps.setString(1,user.getUsername());
			
			ps.execute();
			ps.close();
		}
		catch (Exception e) {
			throw new UserManagerException(e);
		}
		finally {
			if (con != null) {
				try {
					con.close();
				}
				catch (Exception e) {
					throw new UserManagerException(e);
				}
			}
		}

		if (user.isLoggedOn()) {
			this.logout( user );
		}
	}


	public User getUserByUsername(String username) throws UserManagerException {
		User user = null;
		
		if ((username != null) && (username.length() > 0))
		{
			Connection con = null;
	
			try {
				con = pool.getConnection();
	
				con.setReadOnly(true);
	
				PreparedStatement ps = con.prepareStatement("SELECT BENUTZERNAME,PASSWORT,VORNAME,NACHNAME,EMAIL,SCORE,COMMENT,ROLES FROM " + USERS_TABLE + " WHERE BENUTZERNAME=?");
	
				ps.setString(1,username);
				
				ResultSet rs = ps.executeQuery();
				
				if (rs.next()) {
					int    rsPasswort = rs.getInt(2);
					String rsVorname  = rs.getString(3);
					String rsNachname = rs.getString(4);
					String rsEmail    = rs.getString(5);
					double rsScore    = rs.getDouble(6);
					String rsComment  = rs.getString(7);
					String rsRoles    = rs.getString(8);
	
					user = UserImpl.getInstance(this,username,rsPasswort,rsVorname,rsNachname,rsEmail,rsScore,rsComment,rsRoles);
				}
	
				rs.close();
				ps.close();
			}
			catch (Exception e) {
				throw new UserManagerException(e);
			}
			finally {
				if (con != null) {
					try {
						con.close();
					}
					catch (Exception e) {
						throw new UserManagerException(e);
					}
				}
			}
		}

		return user;
	}


	public User getUserById(String userid) throws UserManagerException {
		return this.getUserByUsername( UserImpl.extractUsername( userid ) );
	}


	public UserRole findRole(String rolename) {
		return (UserRole)roles.get(rolename);
	}


	public Collection getRoles() {
		return Collections.unmodifiableCollection( roles.values() );
	}


	protected void assignSystemRoles(User user) {
		if (user.getUsername().equals( superAdmin )) {
			user.addRole( findRole( ROLE_SUPERADMIN ) );
			user.addRole( findRole( ROLE_ADMIN ) );
		}
	}


	public void addUserListener(UserListener listener) {
		listenerList.add(UserListener.class, listener);
	}


	public void removeUserListener(UserListener listener) {
		listenerList.remove(UserListener.class, listener);
	}

	
	 protected void fireUserLoggedIn(User user) {
		 UserEvent event = null;
	     Object[] listeners = listenerList.getListenerList();
	     for (int i = listeners.length-2; i>=0; i-=2) {
	         if (listeners[i]==UserListener.class) {
	             if (event == null) event = new UserEvent(getConsensusFoundationManager(),user);
	             ((UserListener)listeners[i+1]).userLoggedIn(event);
	         }
	     }
	 }

	 
	 protected void fireUserLoggedOut(User user) {
		 UserEvent event = null;
	     Object[] listeners = listenerList.getListenerList();
	     for (int i = listeners.length-2; i>=0; i-=2) {
	         if (listeners[i]==UserListener.class) {
	             if (event == null) event = new UserEvent(getConsensusFoundationManager(),user);
	             ((UserListener)listeners[i+1]).userLoggedOut(event);
	         }
	     }
	 }

	 
	 protected void fireUserCreated(User user) {
		 UserEvent event = null;
	     Object[] listeners = listenerList.getListenerList();
	     for (int i = listeners.length-2; i>=0; i-=2) {
	         if (listeners[i]==UserListener.class) {
	             if (event == null) event = new UserEvent(getConsensusFoundationManager(),user);
	             ((UserListener)listeners[i+1]).userCreated(event);
	         }
	     }
	 }
	 
	 
	protected void saveUserScore(User user) {
		Connection con  = null;
	
		try {
			con = pool.getConnection();
			
			con.setReadOnly(false);
	
			PreparedStatement ps = con.prepareStatement("UPDATE " + USERS_TABLE + " SET SCORE=? WHERE BENUTZERNAME=?");
	
			ps.setDouble(1,user.getScore());
			ps.setString(2,user.getUsername());
			
			ps.execute();
			
			ps.close();
		}
		catch (Exception e) {
			this.getLoggingManager().error("Error writing user score: " + e);
		}
		finally {
			if (con != null) {
				try {
					con.close();
				}
				catch (Exception e) {
					this.getLoggingManager().error("Error writing user score: " + e);
				}
			}
		}
	}


	protected void saveUserComment(User user) {
		Connection con  = null;
		
		try {
			con = pool.getConnection();
			
			con.setReadOnly(false);
	
			PreparedStatement ps = con.prepareStatement("UPDATE " + USERS_TABLE + " SET COMMENT=? WHERE BENUTZERNAME=?");
	
			ps.setString(1,user.getComment());
			ps.setString(2,user.getUsername());
			
			ps.execute();
			
			ps.close();
		}
		catch (Exception e) {
			this.getLoggingManager().error("Error writing user comment: " + e);
		}
		finally {
			if (con != null) {
				try {
					con.close();
				}
				catch (Exception e) {
					this.getLoggingManager().error("Error writing user comment: " + e);
				}
			}
		}
	}


	protected void saveUserRoles(User user) {
		String rolestr = null;

		Iterator it = user.getRoles().iterator();
		
		while (it.hasNext()) {
			UserRole role = (UserRole)it.next();
			if (rolestr == null) {
				rolestr = "";
			}
			else {
				rolestr += ",";
			}
			rolestr += role.getRolename();
		}

		Connection con  = null;
		
		try {
			con = pool.getConnection();
			
			con.setReadOnly(false);
	
			PreparedStatement ps = con.prepareStatement("UPDATE " + USERS_TABLE + " SET ROLES=? WHERE BENUTZERNAME=?");
	
			ps.setString(1,rolestr);
			ps.setString(2,user.getUsername());
			
			ps.execute();
			
			ps.close();
		}
		catch (Exception e) {
			this.getLoggingManager().error("Error writing user roles: " + e + " (" + rolestr + ")");
		}
		finally {
			if (con != null) {
				try {
					con.close();
				}
				catch (Exception e) {
					this.getLoggingManager().error("Error writing user roles: " + e);
				}
			}
		}
	}


	protected void loadUserData(UserImpl user) {
		Connection con  = null;

		try {
			con = pool.getConnection();

			con.setReadOnly(true);

			PreparedStatement ps = con.prepareStatement("SELECT BENUTZERNAME,PASSWORT,VORNAME,NACHNAME,EMAIL,SCORE,COMMENT,ROLES FROM " + USERS_TABLE + " WHERE BENUTZERNAME=?");
			
			ps.setString(1,user.getUsername());
			
			ResultSet rs = ps.executeQuery();
			
			if (rs.next()) {
				int    rsPasswort = rs.getInt(2);
				String rsVorname  = rs.getString(3);
				String rsNachname = rs.getString(4);
				String rsEmail    = rs.getString(5);
				double rsScore    = rs.getDouble(6);
				String rsComment  = rs.getString(7);
				String rsRoles    = rs.getString(8);

				user.init(rsPasswort,rsVorname,rsNachname,rsEmail,rsScore,rsComment,rsRoles);
			}

			rs.close();
			ps.close();
		}
		catch (Exception e) {
			this.getLoggingManager().error("Error loading user data: " + e);
		}
		finally {
			if (con != null) {
				try {
					con.close();
				}
				catch (Exception e) {
					this.getLoggingManager().error("Error loading user data: " + e);
				}
			}
		}
	}


	private void initDatabase() throws Exception {
		Connection con = null;

		try {
			con = pool.getConnection();
			
			con.setReadOnly(false);
			
			Statement stmt = con.createStatement();

			stmt.execute("CREATE TABLE IF NOT EXISTS `" + USERS_TABLE + "` (\n"
					+ "`BENUTZERNAME` VARCHAR(50) NOT NULL PRIMARY KEY,\n"
					+ "`PASSWORT` INT,\n"
					+ "`VORNAME` VARCHAR(50),\n"
					+ "`NACHNAME` VARCHAR(50),\n"
					+ "`EMAIL` VARCHAR(50),\n"
					+ "`SCORE` DOUBLE,\n"
					+ "`COMMENT` VARCHAR(4096),\n"
					+ "`ROLES` VARCHAR(255)\n"
					+ ")\n"
					+ "ENGINE = MYISAM\n"
					+ "CHARACTER SET utf8;");
			
			stmt.close();
		}
		finally {
			if (con != null) {
				con.close();
			}
		}
	}


	private void createRole(String rolename, String description) {
		UserRole role = new UserRoleImpl(this,rolename,description);
		roles.put(rolename,role);
	}


	private void loginUser(UserImpl user) {
		user.setLoggedOn(true);
		this.getLoggingManager().info("login " + user);
		fireUserLoggedIn(user);
	}


	public String getVersion() {
		return "2006-06-14 tm (" + this.getClass().getName() + ")";
	}


	public Iterator getUsers()  throws UserManagerException {
		Connection con  = null;

		try {
			con = pool.getConnection();

			con.setReadOnly(true);

			PreparedStatement ps = con.prepareStatement( "SELECT BENUTZERNAME,PASSWORT,VORNAME,NACHNAME,EMAIL,SCORE,COMMENT,ROLES FROM " + USERS_TABLE + " ORDER BY BENUTZERNAME" );
			
			ResultSet rs = ps.executeQuery();
			
			return new UserIterator( con, rs );
		}
		catch (Exception e) {
			if (con != null) {
				try {
					con.close();
				}
				catch (Exception e2) {
					this.getLoggingManager().error("Error reading users: " + e2);
				}
			}
			throw new UserManagerException(e);
		}
	}



	private class UserIterator implements Iterator {
		private Connection con;
		private ResultSet  rs;
		private boolean hasnext;
		
		private UserIterator(Connection con, ResultSet rs) throws Exception {
			this.con = con;
			this.rs  = rs;
			readHasNext();
		}

		public boolean hasNext() {
			return this.hasnext;
		}

		private void readHasNext() throws Exception {
			this.hasnext = rs.next();
			if (!hasnext) {
				this.close();
			}
		}

		public Object next() {
			if (!hasnext) return null;
			try {
				String rsUsername = rs.getString(1);
				int    rsPasswort = rs.getInt(2);
				String rsVorname  = rs.getString(3);
				String rsNachname = rs.getString(4);
				String rsEmail    = rs.getString(5);
				double rsScore    = rs.getDouble(6);
				String rsComment  = rs.getString(7);
				String rsRoles    = rs.getString(8);

				User user = UserImpl.getInstance( UserManagerImpl.this, rsUsername, rsPasswort, rsVorname, rsNachname, rsEmail, rsScore, rsComment, rsRoles );
				
				readHasNext();
				
				return user;
			}
			catch (Exception e) {
				getLoggingManager().error("Error iterating users: " + e);
				this.close();
				return null;
			}
		}

		public void remove() {
			throw new UnsupportedOperationException();
		}			

		protected void finalize() throws Throwable {
			this.close();
			super.finalize();
		}
		
		private void close() {
			if (rs != null) {
				try {
					rs.close();
				}
				catch (Exception e) {
					getLoggingManager().error("Error iterating users: " + e);
				}
				rs = null;
			}
			if (con != null) {
				try {
					con.close();
				}
				catch (Exception e) {
					getLoggingManager().error("Error iterating users: " + e);
				}
				con = null;
			}
		}
	}

}
