package org.ultramine.mods.hawkeye.database;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import net.minecraft.server.MinecraftServer;
import net.minecraft.world.WorldServer;

import org.ultramine.mods.hawkeye.DataType;
import org.ultramine.mods.hawkeye.HawkEye;
import org.ultramine.mods.hawkeye.SearchParser;
import org.ultramine.mods.hawkeye.callbacks.BaseCallback;
import org.ultramine.mods.hawkeye.callbacks.DeleteCallback;
import org.ultramine.mods.hawkeye.callbacks.RebuildCallback;
import org.ultramine.mods.hawkeye.callbacks.RollbackCallback;
import org.ultramine.mods.hawkeye.entry.DataEntry;
import org.ultramine.mods.hawkeye.util.HawkUtil;
import org.ultramine.server.util.BasicTypeParser;
import org.ultramine.server.util.GlobalExecutors;

/**
 * Threadable class for performing a search query Used for in-game searches and
 * rollbacks
 * 
 * @author oliverw92
 */
public class SearchQuery implements Runnable
{
	private final SearchParser parser;
	private final SearchDir dir;
	private final BaseCallback callBack;
	private final boolean delete;

	public SearchQuery(BaseCallback callBack, SearchParser parser, SearchDir dir)
	{
		this.callBack = callBack;
		this.parser = parser;
		this.dir = dir;
		this.delete = (callBack instanceof DeleteCallback);

		//Start thread
		GlobalExecutors.cachedIO().execute(this);
	}

	/**
	 * Run the search query
	 */
	@Override
	public void run()
	{
		HawkUtil.debug("Beginning search query");
		StringBuilder sql = new StringBuilder(512);

		if(delete)
			sql.append("DELETE FROM ");
		else
			sql.append("SELECT * FROM ");

		sql.append("`" + HawkEye.instance.config.database.dataTable + "`");

		boolean getsubdata = HawkEye.instance.config.general.logBlocksSubData && (callBack instanceof RollbackCallback || callBack instanceof RebuildCallback);
		if(getsubdata)
			sql.append(" LEFT JOIN `" + HawkEye.instance.config.database.subdataTable + "` USING(`data_id`) WHERE ");
		else
			sql.append(" WHERE ");

		List<String> args = new LinkedList<String>();
		List<Object> binds = new LinkedList<Object>();

		//Match players from database list
		HawkUtil.debug("Building players");
		if(parser.players.size() > 0)
		{
			List<Integer> pids = new ArrayList<Integer>();
			List<Integer> npids = new ArrayList<Integer>();
			for(String player : parser.players)
			{
				String name = player.toLowerCase();
				boolean not = name.charAt(0) == '!';
				if(not)
					name = name.substring(1);
				Integer id = DataManager.dbPlayers.get(name);
				if(id != null)
				{
					if(not)
						npids.add(id);
					else
						pids.add(id);
				}
			}
			//Include players
			if(pids.size() > 0)
				args.add("player_id IN (" + HawkUtil.join(pids, ",") + ")");
			//Exclude players
			if(npids.size() > 0)
				args.add("player_id NOT IN (" + HawkUtil.join(npids, ",") + ")");
			if(npids.size() + pids.size() < 1)
			{
				callBack.error(SearchError.NO_PLAYERS, "No players found matching your specifications");
				return;
			}
		}

		//Match worlds from database list
		HawkUtil.debug("Building worlds");
		if(parser.worlds != null)
		{
			List<Integer> wids = new ArrayList<Integer>();
			List<Integer> nwids = new ArrayList<Integer>();
			for(String name : parser.worlds)
			{
				name = name.toLowerCase();
				boolean not = name.charAt(0) == '!';
				if(not)
					name = name.substring(1);

				int dim;
				if(BasicTypeParser.isInt(name))
				{
					dim = Integer.parseInt(name);
				}
				else
				{
					WorldServer world = MinecraftServer.getServer().getMultiWorld().getWorldByName(name);
					if(world != null)
						dim = world.provider.dimensionId;
					else
						continue;
				}
				if(not)
					nwids.add(dim);
				else
					wids.add(dim);
			}
			//Include worlds
			if(wids.size() > 0)
				args.add("world_id IN (" + HawkUtil.join(wids, ",") + ")");
			//Exclude worlds
			if(nwids.size() > 0)
				args.add("world_id NOT IN (" + HawkUtil.join(nwids, ",") + ")");
			if(nwids.size() + wids.size() < 1)
			{
				callBack.error(SearchError.NO_WORLDS, "No worlds found matching your specifications");
				return;
			}
		}

		//Compile actions into SQL form
		HawkUtil.debug("Building actions");
		if(parser.actions != null && parser.actions.size() > 0)
		{
			List<Integer> acs = new ArrayList<Integer>();
			for(DataType act : parser.actions)
				acs.add(act.getId());
			args.add("action IN (" + HawkUtil.join(acs, ",") + ")");
		}

		//Add dates
		HawkUtil.debug("Building dates");
		if(parser.dateFrom != 0)
		{
			args.add("date >= ?");
			binds.add(new Timestamp(parser.dateFrom));
		}
		if(parser.dateTo != 0)
		{
			args.add("date <= ?");
			binds.add(new Timestamp(parser.dateTo));
		}

		//Check if location is exact or a range
		HawkUtil.debug("Building location");
		if(parser.minLoc != null)
		{
			args.add("(x BETWEEN " + parser.minLoc.getX() + " AND " + parser.maxLoc.getX() + ")");
			args.add("(y BETWEEN " + parser.minLoc.getY() + " AND " + parser.maxLoc.getY() + ")");
			args.add("(z BETWEEN " + parser.minLoc.getZ() + " AND " + parser.maxLoc.getZ() + ")");
		}
		else if(parser.loc != null)
		{
			args.add("x = " + parser.loc.getX());
			args.add("y = " + parser.loc.getY());
			args.add("z = " + parser.loc.getZ());
		}

		//Build the filters into SQL form
		HawkUtil.debug("Building filters");
		if(parser.filters != null)
		{
			for(String filter : parser.filters)
			{
				args.add("data LIKE ?");
				binds.add("%" + filter + "%");
			}
		}

		//Build WHERE clause
		sql.append(HawkUtil.join(args, " AND "));

		//Add order by
		HawkUtil.debug("Ordering by data_id");
		sql.append(" ORDER BY `data_id` DESC");

		//Check the limits
		HawkUtil.debug("Building limits");
		if(HawkEye.instance.config.general.maxLines > 0)
			sql.append(" LIMIT " + HawkEye.instance.config.general.maxLines);

		//Util.debug("Searching: " + sql);

		//Set up some stuff for the search
		ResultSet res;
		List<DataEntry> results = new ArrayList<DataEntry>();
		JDCConnection conn = DataManager.getConnection();
		PreparedStatement stmnt = null;
		PreparedStatement stmnt2 = null;
		int deleted = 0;

		try
		{
			//Execute query
			stmnt = conn.prepareStatement(sql.toString());

			HawkUtil.debug("Preparing statement");
			for(int i = 0; i < binds.size(); i++)
				stmnt.setObject(i + 1, binds.get(i));

			HawkUtil.debug("Searching: " + stmnt.toString());

			if(delete)
			{
				HawkUtil.debug("Deleting entries");
				deleted = stmnt.executeUpdate();
			}
			else
			{
				res = stmnt.executeQuery();

				HawkUtil.debug("Getting results");

				//Retrieve results
				while(res.next())
				{
					DataEntry de = DataManager.createEntryFromRes(res, getsubdata);
					results.add(de);
				}

				//If ascending, reverse results
				if(dir == SearchDir.ASC)
					Collections.reverse(results);
			}
		}
		catch(Exception ex)
		{
			HawkUtil.error("Error executing MySQL query: ", ex);
			callBack.error(SearchError.MYSQL_ERROR, "Error executing MySQL query: " + ex);
			return;
		}
		finally
		{
			try
			{
				if(stmnt != null)
					stmnt.close();
				if(stmnt2 != null)
					stmnt2.close();
				conn.close();
			}
			catch(SQLException ex)
			{
				HawkUtil.error("Unable to close SQL connection: ", ex);
				callBack.error(SearchError.MYSQL_ERROR, "Unable to close SQL connection: " + ex);
			}

		}

		HawkUtil.debug(results.size() + " results found");

		//Run callback
		if(delete)
			((DeleteCallback) callBack).deleted = deleted;
		else
			callBack.results = results;

		callBack.execute();

		HawkUtil.debug("Search complete");

	}

	/**
	 * Enumeration for result sorting directions
	 * 
	 * @author oliverw92
	 */
	public enum SearchDir
	{
		ASC, DESC
	}

	/**
	 * Enumeration for query errors
	 */
	public enum SearchError
	{
		NO_PLAYERS, NO_WORLDS, MYSQL_ERROR
	}

}
