/**
 * Copyright 2009, Res Interactive, LLC. All Rights Reserved.
 */

package com.tootsville.promo;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Locale;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.starhope.appius.except.AlreadyExistsException;
import org.starhope.appius.except.DataException;
import org.starhope.appius.except.NotFoundException;
import org.starhope.appius.game.AppiusClaudiusCaecus;
import org.starhope.appius.mb.UserEnrolment;
import org.starhope.appius.sql.SQLPeerDatum;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.util.LibMisc;

/**
 * <p>
 * Promotions used for Marketing. These can either be used for tracking
 * links from advertising sites or for peanut codes for marketing
 * events.
 * </p>
 * 
 * @author <a href="mailto:twheys@gmail.com@resinteractive.com">Tim
 *         Heys</a>
 */
public class Promotion extends SQLPeerDatum {
	/**
	 * Types of marketing tracking available.
	 * 
	 * @author <a href="mailto:twheys@gmail.com@resinteractive.com">Tim
	 *         Heys</a>
	 */
	private enum TrackingType {
		/**
		 * Advertisement promotions. Implies that this promotions
		 * provides a link for tracking.
		 */
		ad,
		/**
		 * Event tracking. Implies that a peanut codes are given that
		 * create this referer.
		 */
		event
	}

	/**
	 * Java serialisation unique ID
	 */
	private static final long serialVersionUID = -8498015398061540649L;

	/**
	 * Create a new promotional code series for a specific event
	 * 
	 * @param prefixCode WRITEME
	 * @return WRITEME
	 * @throws AlreadyExistsException WRITEME
	 * @throws DataException WRITEME
	 * @throws IndexOutOfBoundsException WRITEME
	 */
	public static Promotion createEventPromotion (
			final String prefixCode)
	throws IndexOutOfBoundsException, DataException,
	AlreadyExistsException {
		return new Promotion (prefixCode, prefixCode + "Event",
				"Event code for " + prefixCode, TrackingType.event);
	}

	/**
	 * Get an Advertisement Promotion object from a tracking link. Use
	 * this instead of {@link #getByLink(String)} to only retrieve
	 * promotions labeled as an advertisement type.
	 * 
	 * @param maybe The link in question.
	 * @return a Promotion if there is one matching the link.
	 * @throws NotFoundException if there is no such ad link matching
	 *             param maybe.
	 */
	public static Promotion getAdByLink (final String maybe)
	throws NotFoundException {
		System.out.println ("Searching for promotions with link: ("
				+ maybe + ")");
		final Promotion p = Promotion.getByLink (maybe);
		if (TrackingType.ad != p.getType ())
			throw new NotFoundException (
					"No such AD promotion for the link: " + maybe);
		return p;
	}

	/**
	 * Get a Promotion object from a tracking link.
	 * 
	 * @param maybe The link in question.
	 * @return a Promotion if there is one matching the link.
	 * @throws NotFoundException if there is no such link matching param
	 *             maybe.
	 */
	public static Promotion getByLink (final String maybe)
	throws NotFoundException {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con.prepareStatement ("SELECT * FROM promotions WHERE link=?");
			st.setString (1, maybe);
			st.execute ();
			rs = st.getResultSet ();
			if (rs.next ()) return new Promotion (rs);
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
			throw new NotFoundException (
					"No such promotion for the link: " + maybe);
		} finally {
			LibMisc.closeAll (rs, st, con);
		}
		throw new NotFoundException (maybe);
	}

	/**
	 * 4 letter tracking code for storing a referer for User.
	 */
	private char [] code = new char [4];

	/**
	 * Description of this promotion.
	 */
	private String description = "";

	/**
	 * The link used for advertisement links. link is a relative link
	 * with the format: https://members.tootsville.com/l/{#link} Only
	 * provide the last part of the link.
	 */
	private String link;

	/**
	 * The ID of this promotion
	 */
	private int promotionID;

	/**
	 * URL to where the tracking link redirects to.
	 */
	private String redirect;

	/**
	 * The Tracking type for this promotion.
	 * 
	 * @see TrackingType
	 */
	TrackingType type;

	/**
	 * Create a Promotion object from a result set. Calls
	 * {@link #set(ResultSet)}.
	 * 
	 * @param rs The result set of a Promotion.
	 * @throws SQLException if the result set can't be interpreted
	 */
	protected Promotion (final ResultSet rs) throws SQLException {
		set (rs);
	}

	/**
	 * Create a new promotion. Code must follow be 4 letters exactly.
	 * Link must exist as a unique value.
	 * 
	 * @see TrackingType
	 * @param newCode The code 4 letter tracking code for storing a
	 *            referrer for User.
	 * @param newLink The link to this promotion. Follows the format of
	 *            https://members.tootsville.com/l/{link}
	 * @param newRedirect The redirect link for this link if it is other
	 *            than default.
	 * @param newDescription The description for this promotion.
	 * @param newType The type of this promotion.
	 * @throws IndexOutOfBoundsException if the code is not 4 characters
	 *             long.
	 * @throws DataException if the code contains characters other than
	 *             letters.
	 * @throws AlreadyExistsException if a Promotion with this link
	 *             already exists.
	 */
	public Promotion (final String newCode, final String newLink,
			final String newRedirect, final String newDescription,
			final TrackingType newType)
	throws IndexOutOfBoundsException, DataException,
	AlreadyExistsException {
		assertPromotionLinkAvailable (newLink);

		final String newCodeString = newCode
		.toLowerCase (Locale.ENGLISH);
		if (4 != newCode.length ())
			throw new IndexOutOfBoundsException ();
		final Pattern lettersOnly = Pattern.compile ("[a-zA-Z]+");
		final Matcher letterMatcher = lettersOnly
		.matcher (newCodeString);
		if ( !letterMatcher.matches ())
			throw new DataException ("Code (" + newCodeString
					+ ") contains characters other than letters.");
		code = newCodeString.toCharArray ();

		link = newLink;
		redirect = newRedirect;
		description = newDescription;
		type = newType;

		promotionID = -1;
		insert ();
	}

	/**
	 * Creates a promotion with a redirect link set as the default
	 * "http://www.tootsville.com"
	 * 
	 * @see #Promotion(String, String, String, String, TrackingType)
	 * @see TrackingType
	 * @param newCode The code 4 letter tracking code for storing a
	 *            referer for User.
	 * @param newLink The link to this promotion. Follows the format of
	 *            https://members.tootsville.com/l/{link}
	 * @param newDescription The description for this promotion.
	 * @param newType The type of this promotion.
	 * @throws IndexOutOfBoundsException if the code is not 4 characters
	 *             long.
	 * @throws DataException if the code contains characters other than
	 *             letters.
	 * @throws AlreadyExistsException if a Promotion with this link
	 *             already exists.
	 */
	public Promotion (final String newCode, final String newLink,
			final String newDescription, final TrackingType newType)
	throws IndexOutOfBoundsException, DataException,
	AlreadyExistsException {
		this (newCode, newLink, "http://www.tootsville.com/",
				newDescription, newType);
	}

	/**
	 * Check if the link already exists.
	 * 
	 * @param newLink The link in question.
	 * @throws AlreadyExistsException if the link already exists.
	 */
	private void assertPromotionLinkAvailable (final String newLink)
	throws AlreadyExistsException {
		try {
			Promotion.getByLink (newLink);
			throw new AlreadyExistsException (
					"Promotion already exists with the link " + newLink);
		} catch (final NotFoundException e) {
			// NotFoundException implies that this code is available,
			// this is the goal of this method.
			return;
		}
	}

	/**
	 * @see org.starhope.appius.sql.SQLPeerDatum#changed()
	 */
	@Override
	public void changed () {
		flush ();
	}

	/**
	 * <pre>
	 * twheys@gmail.com Jan 7, 2010
	 * </pre>
	 * 
	 * TO createPeanutCodesForPromotion WRITEME...
	 * 
	 * @param numberOfCodes WRITEME
	 * @param minValue WRITEME
	 * @param maxValue WRITEME
	 */
	public void createPeanutCodesForPromotion (final int numberOfCodes,
			final int minValue, final int maxValue) {
		for (int i = 0; i < numberOfCodes; i++ ) {
			final String pnutCodeSerial = generatePeanutCode (String
					.valueOf (code));
			new PeanutCode (pnutCodeSerial, minValue, maxValue);
		}
	}

	/**
	 * @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 promotions SET type=?, description=?, link=?, "
					+ "redirect=?, code=? WHERE ID=?");
			st.setString (1, type.toString ());
			st.setString (2, description);
			st.setString (3, link);
			st.setString (4, redirect);
			st.setString (5, String.valueOf (code));
			st.setInt (6, promotionID);
		} catch (final SQLException e) {
			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 */
				}
			}

		}
	}

	/**
	 * Create a pseudorandom, unique order code consisting of the
	 * approved letters and numbers. (Excludes the letters O, I, Z, and
	 * S for potential confusion with 0, 1, 2, and 5)
	 * 
	 * @param prefix prefix code for this event/product
	 * @return an unique peanut code
	 */
	private String generatePeanutCode (final String prefix) {
		final String peanutCode = prefix;
		boolean found = false;
		final Random rnd = new Random ();

		PreparedStatement st = null;
		Connection con = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con.prepareStatement ("SELECT COUNT(*) FROM serialNumbers WHERE serial=?");
			final char [] codeChars = UserEnrolment
			.getOrderCodeChars ();
			do {
				final StringBuilder newCode = new StringBuilder ();
				// generate 6 random characters from the given
				for (int i = 0; i < 6; ++i) {
					newCode.append (codeChars [(int) (rnd.nextDouble () * codeChars.length)]);
				}
				st.setString (1, newCode.toString ());
				if (st.execute ()) {
					final ResultSet exists = st.getResultSet ();
					exists.next ();
					if (exists.getInt (1) == 0) {
						found = true;
					}
				}
			} while ( !found);
		} 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 */
				}
			}
		}
		System.out.println ("Generated peanut code: " + peanutCode);
		return peanutCode;
	}

	/**
	 * @see org.starhope.appius.sql.SQLPeerDatum#getCacheUniqueID()
	 */
	@Override
	protected String getCacheUniqueID () {
		return "Promotion-" + getLink ();
	}

	/**
	 * @return the code for this promotion
	 * @see #code
	 */
	public String getCode () {
		return String.valueOf (code);
	}

	/**
	 * @return the description for this promotion
	 * @see #description
	 */
	public String getDescription () {
		return description;
	}

	/**
	 * @return the link for this promotion
	 * @see #link
	 */
	public String getLink () {
		return link;
	}

	/**
	 * @return the redirect link for this promotion
	 * @see #redirect
	 */
	public String getRedirect () {
		return redirect;
	}

	/**
	 * @return the type for this promotion
	 * @see #type
	 */
	public TrackingType getType () {
		return type;
	}

	/**
	 * <pre>
	 * twheys@gmail.com Jan 7, 2010
	 * </pre>
	 * 
	 * TO insert WRITEME...
	 */
	private void insert () {
		Connection con = null;
		PreparedStatement insert = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			insert = con
			.prepareStatement (
					"INSERT INTO promotions (type, description, link, redirect, code) VALUES (?, ?, ?, ?, ?)",
					Statement.RETURN_GENERATED_KEYS);
			insert.setString (1, type.toString ());
			insert.setString (2, description);
			insert.setString (3, link);
			insert.setString (4, redirect);
			insert.setString (5, String.valueOf (code));

			if (insert.executeUpdate () != 1)
				throw new SQLException (
						"adding new promotion failed with "
						+ insert.getUpdateCount ()
						+ " updates.");
			final ResultSet keys = insert.getGeneratedKeys ();
			if (keys.next ()) {
				promotionID = keys.getInt (1);
			} else throw new SQLException ("Can't get user ID");
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} finally {
			if (null != insert) {
				try {
					insert.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
	}

	/**
	 * @see org.starhope.appius.sql.SQLPeerDatum#set(java.sql.ResultSet)
	 */
	@Override
	protected void set (final ResultSet rs) throws SQLException {
		promotionID = rs.getInt ("id");
		type = TrackingType.valueOf (rs.getString ("type"));
		description = rs.getString ("description");
		link = rs.getString ("link");
		redirect = rs.getString ("redirect");
		code = rs.getString ("code").toCharArray ();
	}

	/**
	 * Set the code for this promotion. The code MUST be 4 characters
	 * exactly and contain ONLY letters. The letter characters will
	 * automatically be converted to lower case.
	 * 
	 * @param newCodeString a 4 character string of only letters.
	 * @throws IndexOutOfBoundsException if the code is not 4 characters
	 *             long.
	 * @throws DataException if the code contains characters other than
	 *             letters.
	 */
	public void setCode (final String newCodeString)
	throws IndexOutOfBoundsException, DataException {
		final String newCode = newCodeString
		.toLowerCase (Locale.ENGLISH);
		if (4 != newCode.length ())
			throw new IndexOutOfBoundsException ();
		final Pattern lettersOnly = Pattern.compile ("[a-z]");
		final Matcher letterMatcher = lettersOnly.matcher (newCode);
		if ( !letterMatcher.matches ())
			throw new DataException ("Code (" + newCode
					+ ") contains characters other than letters.");
		code = newCode.toCharArray ();
		changed ();
	}

	/**
	 * @param newDescription the description of this promotion.
	 * @see #description
	 */
	public void setDescription (final String newDescription) {
		description = newDescription;
		changed ();
	}

	/**
	 * @param newLink the tracking link for this promotion.
	 * @see #link
	 */
	public void setLink (final String newLink) {
		link = newLink;
		changed ();
	}

	/**
	 * @param newRedirectLink the redirect to set
	 * @see #redirect
	 */
	public void setRedirect (final String newRedirectLink) {
		redirect = newRedirectLink;
		changed ();
	}

	/**
	 * @param newType the type to set
	 * @see #type
	 */
	public void setType (final TrackingType newType) {
		type = newType;
		changed ();
	}
}
