/**
 * <p>
 * Copyright © 2009-2010, Bruce-Robert Pocock
 * </p>
 * <p>
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at
 * your option) any later version.
 * </p>
 * <p>
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * </p>
 * <p>
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 * </p>
 * 
 * @author brpocock,theys
 */

package org.starhope.appius.user;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.*;
import java.sql.Date;
import java.util.*;

import javax.naming.NamingException;

import org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.except.*;
import org.starhope.appius.game.AppiusClaudiusCaecus;
import org.starhope.appius.messaging.Mail;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.util.LibMisc;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
 * This class represents the parent of a kid or teen account
 * 
 * @see User#getParent()
 * @author brpocock,theys
 */
public class Parent extends Person {
	/**
	 * Java serialization unique ID
	 */
	private static final long serialVersionUID = -793284706598303053L;

	/**
	 * @param cookie the approval cookie uniquely identifying the
	 *        desired Parent
	 * @return the Parent uniquely identified by the given approval
	 *         cookie
	 * @throws NotFoundException if the cookie does not uniquely
	 *         identify any Parent
	 * @throws IOException if the contents of the approval cookie can't
	 *         be decoded
	 */
	public static Parent getByApprovalCookie (final String cookie)
	throws NotFoundException, IOException {
		final BASE64Decoder decoder = new BASE64Decoder ();
		final String info = new String (decoder.decodeBuffer (cookie));
		final String [] infos = info.split ("/");
		if (infos.length < 2) throw new NotFoundException (cookie);
		Parent tryThis = Parent.getByID (Integer.parseInt (infos [0]));
		if (null == tryThis) {
			tryThis = Parent.getByMail (infos [1]);
		}
		if (null == tryThis) throw new NotFoundException (cookie);
		return tryThis;
	}

	/**
	 * @param id database ID number
	 * @return the relevant Parent record (if it exists), or null if
	 *         not.
	 */
	public static Parent getByID (final int id) {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT * FROM parents WHERE id=?");

			st.setInt (1, id);
			if (st.execute ()) {
				rs = st.getResultSet ();
				if (rs.next ()) return new Parent (rs);
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
			// No op. Just return null.
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}

		return null;
	}

	/**
	 * @param mail The parent's eMail address
	 * @return the relevant Parent record, or null if there is none
	 */
	public static Parent getByMail (final String mail) {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT * FROM parents WHERE mail=?");
			st.setString (1, mail);
			if (st.execute ()) {
				rs = st.getResultSet ();
				if (rs.next ()) {

					final Parent p = new Parent (rs);

					return p;
				}
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
			// No op. Just return null.
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}

		return null;
	}

	/**
	 * @param parentMail the mail address for the parent
	 * @return a new parent record or existing one; never null.
	 */
	public static Parent getOrCreateByMail (final String parentMail) {
		final Parent found = Parent.getByMail (parentMail);
		if (null != found) return found;
		try {
			return new Parent (parentMail);
		} catch (final AlreadyExistsException e) {
			return Parent.getOrCreateByMail (parentMail);
		}
	}

	/**
	 * Database ID value
	 */
	private int parentID;

	/**
	 * @param rs resultset describing a parent
	 */
	public Parent (final ResultSet rs) {
		super ();
		try {
			set (rs);
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		}
	}

	/**
	 * Create a new Parent record
	 * 
	 * @param parentMail the address of the parent
	 * @throws AlreadyExistsException if a parent record already exists
	 *             with the given eMail address
	 */
	public Parent (final String parentMail)
	throws AlreadyExistsException {
		super ();
		try {
			setMail (parentMail);
		} catch (final GameLogicException e) {
			AppiusClaudiusCaecus.fatalBug ("impossible");
		}
		password = null;
		mailConfirmed = null;
		mailConfirmSent = null;
		insert ();
	}

	/**
	 * Create a new parent record and set an initial password at the
	 * same time
	 * 
	 * @param newMail mail address
	 * @param newPassword password
	 * @throws AlreadyExistsException if the user has an existing
	 *             account, and they know the password
	 * @throws PrivilegeRequiredException if the mail exists, but the
	 *             password is wrong
	 */
	public Parent (final String newMail, final String newPassword)
	throws PrivilegeRequiredException, AlreadyExistsException {
		super ();
		final Parent p = Parent.getByMail (newMail);
		if (null != p) {
			if (p.getPassword ().equals (newPassword))
				throw new AlreadyExistsException (newMail);
			throw new PrivilegeRequiredException ("password");
		}
		try {
			setMail (newMail);
		} catch (final GameLogicException e) {
			AppiusClaudiusCaecus.fatalBug (e);
		}
		setPassword (newPassword);
		insert ();
	}

	/**
	 * @see org.starhope.appius.sql.SQLPeerDatum#flush()
	 */
	@Override
	public void flush () {
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("UPDATE parents SET mail=?, mailConfirmSent=?, mailConfirmed=?, "
					+ "canContact=?, givenName=?, password=?, passRecoveryQ=?, passRecoveryA=? WHERE id=?");
			st.setString (1, getMail ());
			if (null == mailConfirmSent) {
				st.setNull (2, java.sql.Types.DATE);
			} else {
				st.setDate (2, mailConfirmSent);
			}
			if (null == mailConfirmed) {
				st.setNull (3, java.sql.Types.DATE);
			} else {
				st.setDate (3, mailConfirmed);
			}
			st.setString (4, canContact ? "Y" : "N");
			if (null == givenName) {
				st.setNull (5, java.sql.Types.VARCHAR);
			} else {
				st.setString (5, givenName);
			}
			if (null == password) {
				st.setNull (6, java.sql.Types.VARCHAR);
			} else {
				st.setString (6, password);
			}
			st.setString (7, forgotPasswordQuestion);
			st.setString (8, forgotPasswordAnswer);
			st.setInt (9, parentID);
			st.execute ();
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} finally {
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
	}

	/**
	 * @return get an approval cookie which can be used to uniquely
	 *         identify this Parent
	 */
	@Override
	public String getApprovalCookie () {
		final BASE64Encoder encoder = new BASE64Encoder ();
		return encoder
		.encodeBuffer ( (String.valueOf (parentID) + "/" + String
				.valueOf (mail)).getBytes ());
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#getCacheUniqueID()
	 */
	@Override
	protected String getCacheUniqueID () {
		return String.valueOf (parentID);
	}

	/**
	 * This will load up to 2×maxChildren children for an account. If
	 * the user account has more children, it will silently ignore
	 * additional children.
	 * 
	 * @return all children associated with this parent
	 */
	public User [] getChildren () {
		final HashSet <AbstractUser> kids = new HashSet <AbstractUser> ();
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT ID FROM users WHERE parentID=? ORDER BY ID LIMIT ?");

			st.setInt (1, getID ());
			st.setInt (2, AppiusConfig.getIntOrDefault (
					"org.starhope.appius.parent.maxChildren", 10) * 2);
			if (st.execute ()) {
				rs = st.getResultSet ();
				final Vector <Integer> kidIDs = new Vector <Integer> ();
				while (rs.next ()) {
					kidIDs.add (rs.getInt (1));
				}
				final Iterator <Integer> kidNum = kidIDs.iterator ();
				while (kidNum.hasNext ()) {
					kids.add (User.getByID (kidNum.next ()));
				}
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		final User kidsArray[] = kids.toArray (new User [kids.size ()]);
		try {
			Arrays.sort (kidsArray);
		} catch (final Exception e) {
			// don't sort WRITEME
		}

		return kidsArray;
	}

	/**
	 * Get the name of the template file to be used to confirm accounts
	 * of this type.
	 * 
	 * @see org.starhope.appius.user.Person#getConfirmationTemplate()
	 */
	@Override
	public String getConfirmationTemplate () {
		return "ParentConfirmation";
	}

	/**
	 * @see org.starhope.appius.user.Person#getDisplayName()
	 */
	@Override
	public String getDisplayName () {
		if (null != givenName) return givenName;
		final User [] children = getChildren ();
		if (null == children || children.length == 0) {
			// no op
		} else if (children.length > 1) {
			final LinkedList <String> childrensNames = new LinkedList <String> ();
			int count = 0;
			for (final User child : children) {
				if (count++ < 2) {
					childrensNames.add (child.getDisplayName ());
				} else {
					break;
				}
			}
			final int kids = children.length - 2;
			String tag;
			if (kids == 1) {
				tag = "1 other.";
				childrensNames.add (tag);
			} else if (kids > 1) {
				tag = kids + " others.";
				childrensNames.add (tag);
			}

			return "Parent of "
			+ LibMisc.listToDisplay (childrensNames, language,
					dialect);
		} else return "Parent of " + children [0].getDisplayName ();

		if (null != mail) return "<" + mail + ">";
		return "Parent";
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.Person#getHistory(java.sql.Date,int)
	 */
	@Override
	public HashMap <Timestamp, HashMap <String, String>> getHistory (
			final Date after, final int limit) {
		// TODO
		return new HashMap <Timestamp, HashMap <String, String>> ();
	}

	/**
	 * Return the parent record's database ID
	 * 
	 * @return the parent ID
	 */
	public int getID () {
		return parentID;
	}

	/**
	 * WRITEME: document this method (twheys, Aug 05, 2009)
	 * 
	 * @return WRITEME
	 */
	public String getName () {
		return givenName;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.Person#getPotentialUserName()
	 */
	@Override
	public String getPotentialUserName () {
		return getMail ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.Person#getResponsibleMail()
	 */
	@Override
	public String getResponsibleMail () {
		return mail;
	}

	/**
	 * @return true, if this parent has any child account that is banned
	 */
	public boolean hasBannedKids () {
		for (final User child : getChildren ()) {
			if (child.isBanned ())
				return true;
		}
		return false;
	}

	/**
	 * Insert a new parent record into the database, saving the parent
	 * eMail address and password.
	 * 
	 * @throws AlreadyExistsException if the record already exists
	 */
	private void insert () throws AlreadyExistsException {
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement (
					"INSERT INTO parents (mail, password) VALUES (?, ?)",
					Statement.RETURN_GENERATED_KEYS);
			st.setString (1, mail);
			if (null == password) {
				st.setNull (2, java.sql.Types.VARCHAR);
			} else {
				st.setString (2, password);
			}
			st.execute ();
			final ResultSet newID = st.getGeneratedKeys ();
			newID.next ();
			parentID = newID.getInt (1);
		} catch (final SQLException e) {
			if (e.getMessage ().toLowerCase (Locale.ENGLISH).contains (
			"duplicate"))
				throw new AlreadyExistsException (e.getMessage ());
			throw AppiusClaudiusCaecus.fatalBug (e);
		} finally {
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
	}

	/**
	 * @return true, if the account has been registered (has a real
	 *         password)
	 */
	public boolean isRegistered () {
		return null != password && !password.equals ("\n")
		&& !password.equals ("");
	}

	/**
	 * assert that the mail
	 */
	public void mailIsConfirmed () {
		mailConfirmed = new Date (System.currentTimeMillis ());
	}

	/**
	 * This is an overriding method. If any of this parent's children
	 * are staff members, resets their passwords as well as the parent
	 * account password and sends a flurry of reminder mails.
	 * 
	 * @see org.starhope.appius.user.Person#remindPassword()
	 */
	@Override
	protected void remindPassword () {
		for (final User kid : getChildren ()) {
			if (kid.hasStaffLevel (1)) {
				kid.generateNewPassword ();
				kid.remindPassword ();
				generateNewPassword ();
			}
		}
		super.remindPassword ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.Person#rename(java.lang.String)
	 */
	@Override
	public void rename (final String newName) {
		AppiusClaudiusCaecus
		.reportBug ("Spurious attempt to rename a parent, ignored");
	}

	/**
	 * @param user send a notification for the given child user, to
	 *        request parental approval
	 */
	public void requestApproval (final User user) {
		sendNotificationForChild (user);
	}

	/**
	 * <p>
	 * Send a notification to the parent that their child has registered
	 * an account, giving instructions on how to approve the account.
	 * </p>
	 * 
	 * @param kid the user who is a child of this parent
	 */
	public void sendNotificationForChild (final User kid) {
		for (final User child : getChildren ()) {
			if (child.getUserID () == kid.getUserID ()) {
				if ( !kid.isApproved ()) {
					try {
						Mail.sendChildSignupMail (kid);
						return;
					} catch (final FileNotFoundException e) {
						AppiusClaudiusCaecus
						.reportBug (
								"Can't find parent notification template file!",
								e);
					} catch (final IOException e) {
						AppiusClaudiusCaecus.reportBug (
								"Can't send parent notification mail",
								e);
					} catch (final NotFoundException e) {
						AppiusClaudiusCaecus.reportBug (e);
					} catch (final DataException e) {
						AppiusClaudiusCaecus.reportBug (e);
					} catch (final NamingException e) {
						AppiusClaudiusCaecus
						.reportBug (
								"Can't look up domain name for parent notification mail, DNS failure?",
								e);
					}
				}
			}
		}
	}

	/**
	 * @see org.starhope.appius.sql.SQLPeerDatum#set(java.sql.ResultSet)
	 */
	@Override
	protected void set (final ResultSet rs) throws SQLException {
		parentID = rs.getInt ("id");
		mail = rs.getString ("mail");
		mailConfirmSent = rs.getDate ("mailConfirmSent");
		if (rs.wasNull ()) {
			mailConfirmSent = null;
		}
		mailConfirmed = rs.getDate ("mailConfirmed");
		if (rs.wasNull ()) {
			mailConfirmed = null;
		}
		canContact = rs.getString ("canContact").equals ("Y");
		givenName = rs.getString ("givenName");
		if (rs.wasNull ()) {
			givenName = null;
		}
		password = rs.getString ("password");
		if (rs.wasNull ()) {
			password = null;
		}
		forgotPasswordQuestion = rs.getString ("passRecoveryQ");
		forgotPasswordAnswer = rs.getString ("passRecoveryA");
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#toJSON()
	 */
	@Override
	public JSONObject toJSON () {
		final JSONObject self = new JSONObject ();
		try {
			self.put ("mail", getMail ());
		} catch (final JSONException e) {
			// Default catch action, report bug (brpocock, Aug 17, 2009)
			AppiusClaudiusCaecus.reportBug (e);
		}
		return self;
	}

}
