package com.earth2me.essentials;

import static com.earth2me.essentials.I18n._;
import com.earth2me.essentials.api.ITeleport;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.logging.Logger;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;


public class Teleport implements Runnable, ITeleport
{
	private static final double MOVE_CONSTANT = 0.3;
	
	
	private class Target
	{
		private final Location location;
		private final String name;
		
		Target(Location location)
		{
			this.location = location;
			this.name = null;
		}
		
		Target(Player entity)
		{
			this.name = entity.getName();
			this.location = null;
		}
		
		public Location getLocation()
		{
			if (this.name != null)
			{
				
				return ess.getServer().getPlayerExact(name).getLocation();
			}
			return location;
		}
	}
	private IUser user;
	private IUser teleportUser;
	private int teleTimer = -1;
	private long started;	// time this task was initiated
	private long tpdelay;		// how long to delay the teleport
	private int health;
	// note that I initially stored a clone of the location for reference, but...
	// when comparing locations, I got incorrect mismatches (rounding errors, looked like)
	// so, the X/Y/Z values are stored instead and rounded off
	private long initX;
	private long initY;
	private long initZ;
	private Target teleportTarget;
	private boolean respawn;
	private Trade chargeFor;
	private final IEssentials ess;
	private static final Logger logger = Logger.getLogger("Minecraft");
	private TeleportCause cause;
	
	private void initTimer(long delay, Target target, Trade chargeFor, TeleportCause cause)
	{
		initTimer(delay, user, target, chargeFor, cause, false);
	}
	
	private void initTimer(long delay, IUser teleportUser, Target target, Trade chargeFor, TeleportCause cause, boolean respawn)
	{
		this.started = System.currentTimeMillis();
		this.tpdelay = delay;
		this.health = teleportUser.getHealth();
		this.initX = Math.round(teleportUser.getLocation().getX() * MOVE_CONSTANT);
		this.initY = Math.round(teleportUser.getLocation().getY() * MOVE_CONSTANT);
		this.initZ = Math.round(teleportUser.getLocation().getZ() * MOVE_CONSTANT);
		this.teleportUser = teleportUser;
		this.teleportTarget = target;
		this.chargeFor = chargeFor;
		this.cause = cause;
		this.respawn = respawn;
	}
	
	@Override
	public void run()
	{
		
		if (user == null || !user.isOnline() || user.getLocation() == null)
		{
			cancel(false);
			return;
		}
		if (teleportUser == null || !teleportUser.isOnline() || teleportUser.getLocation() == null)
		{
			cancel(false);
			return;
		}
		
		if (!user.isAuthorized("essentials.teleport.timer.move")
			&& (Math.round(teleportUser.getLocation().getX() * MOVE_CONSTANT) != initX
				|| Math.round(teleportUser.getLocation().getY() * MOVE_CONSTANT) != initY
				|| Math.round(teleportUser.getLocation().getZ() * MOVE_CONSTANT) != initZ
				|| teleportUser.getHealth() < health))
		{
			// user moved, cancel teleport
			cancel(true);
			return;
		}
		health = teleportUser.getHealth();  // in case user healed, then later gets injured
		long now = System.currentTimeMillis();
		if (now > started + tpdelay)
		{
			try
			{
				cooldown(false);
				teleportUser.sendMessage(_("teleportationCommencing"));
				try
				{
					if (respawn)
					{
						teleportUser.getTeleport().respawn(cause);
					}
					else
					{
						teleportUser.getTeleport().now(teleportTarget, cause);
					}
					cancel(false);
					if (chargeFor != null)
					{
						chargeFor.charge(user);
					}
				}
				catch (Throwable ex)
				{
					ess.showError(user.getBase(), ex, "teleport");
				}
			}
			catch (Exception ex)
			{
				user.sendMessage(_("cooldownWithMessage", ex.getMessage()));
				if (user != teleportUser)
				{
					teleportUser.sendMessage(_("cooldownWithMessage", ex.getMessage()));
				}
			}
		}
	}
	
	public Teleport(IUser user, IEssentials ess)
	{
		this.user = user;
		this.ess = ess;
	}
	
	public void cooldown(boolean check) throws Exception
	{
		final Calendar time = new GregorianCalendar();
		if (user.getLastTeleportTimestamp() > 0)
		{
			// Take the current time, and remove the delay from it.
			final double cooldown = ess.getSettings().getTeleportCooldown();
			final Calendar earliestTime = new GregorianCalendar();
			earliestTime.add(Calendar.SECOND, -(int)cooldown);
			earliestTime.add(Calendar.MILLISECOND, -(int)((cooldown * 1000.0) % 1000.0));
			// This value contains the most recent time a teleport could have been used that would allow another use.
			final long earliestLong = earliestTime.getTimeInMillis();

			// When was the last teleport used?
			final Long lastTime = user.getLastTeleportTimestamp();
			
			if (lastTime > time.getTimeInMillis())
			{
				// This is to make sure time didn't get messed up on last kit use.
				// If this happens, let's give the user the benifit of the doubt.
				user.setLastTeleportTimestamp(time.getTimeInMillis());
				return;
			}
			else if (lastTime > earliestLong && !user.isAuthorized("essentials.teleport.cooldown.bypass"))
			{
				time.setTimeInMillis(lastTime);
				time.add(Calendar.SECOND, (int)cooldown);
				time.add(Calendar.MILLISECOND, (int)((cooldown * 1000.0) % 1000.0));
				throw new Exception(_("timeBeforeTeleport", Util.formatDateDiff(time.getTimeInMillis())));
			}
		}
		// if justCheck is set, don't update lastTeleport; we're just checking
		if (!check)
		{
			user.setLastTeleportTimestamp(time.getTimeInMillis());
		}
	}

	//If we need to cancel a pending teleport call this method
	public void cancel(boolean notifyUser)
	{
		if (teleTimer == -1)
		{
			return;
		}
		try
		{
			ess.getServer().getScheduler().cancelTask(teleTimer);
			if (notifyUser)
			{
				user.sendMessage(_("pendingTeleportCancelled"));
				if (teleportUser != user)
				{
					teleportUser.sendMessage(_("pendingTeleportCancelled"));
				}
			}
		}
		finally
		{
			teleTimer = -1;
		}
	}

	//The now function is used when you want to skip tp delay when teleporting someone to a location or player.
	public void now(Location loc, boolean cooldown, TeleportCause cause) throws Exception
	{
		if (cooldown)
		{
			cooldown(false);
		}
		now(new Target(loc), cause);
	}
	
	public void now(Player entity, boolean cooldown, TeleportCause cause) throws Exception
	{
		if (cooldown)
		{
			cooldown(false);
		}
		now(new Target(entity), cause);
	}
	
	private void now(Target target, TeleportCause cause) throws Exception
	{
		cancel(false);
		user.setLastLocation();
		user.getBase().teleport(Util.getSafeDestination(target.getLocation()), cause);
	}

	//The teleport function is used when you want to normally teleport someone to a location or player.
	//This method is nolonger used internally and will be removed.
	@Deprecated
	public void teleport(Location loc, Trade chargeFor) throws Exception
	{
		teleport(loc, chargeFor, TeleportCause.PLUGIN);
	}
	
	public void teleport(Location loc, Trade chargeFor, TeleportCause cause) throws Exception
	{
		teleport(new Target(loc), chargeFor, cause);
	}
	
	public void teleport(Player entity, Trade chargeFor, TeleportCause cause) throws Exception
	{
		teleport(new Target(entity), chargeFor, cause);
	}
	
	private void teleport(Target target, Trade chargeFor, TeleportCause cause) throws Exception
	{
		double delay = ess.getSettings().getTeleportDelay();
		
		if (chargeFor != null)
		{
			chargeFor.isAffordableFor(user);
		}
		cooldown(true);
		if (delay <= 0 || user.isAuthorized("essentials.teleport.timer.bypass"))
		{
			cooldown(false);
			now(target, cause);
			if (chargeFor != null)
			{
				chargeFor.charge(user);
			}
			return;
		}
		
		cancel(false);
		warnUser(user, delay);
		initTimer((long)(delay * 1000.0), target, chargeFor, cause);
		
		teleTimer = ess.scheduleSyncRepeatingTask(this, 10, 10);
	}

	//The teleportToMe function is a wrapper used to handle teleporting players to them, like /tphere
	public void teleportToMe(User otherUser, Trade chargeFor, TeleportCause cause) throws Exception
	{
		Target target = new Target(user);
		double delay = ess.getSettings().getTeleportDelay();
		
		if (chargeFor != null)
		{
			chargeFor.isAffordableFor(user);
		}
		cooldown(true);
		if (delay <= 0 || user.isAuthorized("essentials.teleport.timer.bypass"))
		{
			cooldown(false);
			otherUser.getTeleport().now(target, cause);
			if (chargeFor != null)
			{
				chargeFor.charge(user);
			}
			return;
		}
		
		cancel(false);
		warnUser(otherUser, delay);
		initTimer((long)(delay * 1000.0), otherUser, target, chargeFor, cause, false);
		teleTimer = ess.scheduleSyncRepeatingTask(this, 10, 10);
	}
	
	private void warnUser(final IUser user, final double delay)
	{
		Calendar c = new GregorianCalendar();
		c.add(Calendar.SECOND, (int)delay);
		c.add(Calendar.MILLISECOND, (int)((delay * 1000.0) % 1000.0));
		user.sendMessage(_("dontMoveMessage", Util.formatDateDiff(c.getTimeInMillis())));
	}

	//The respawn function is a wrapper used to handle tp fallback, on /jail and /home
	public void respawn(final Trade chargeFor, TeleportCause cause) throws Exception
	{
		double delay = ess.getSettings().getTeleportDelay();
		if (chargeFor != null)
		{
			chargeFor.isAffordableFor(user);
		}
		cooldown(true);
		if (delay <= 0 || user.isAuthorized("essentials.teleport.timer.bypass"))
		{
			cooldown(false);
			respawn(cause);
			if (chargeFor != null)
			{
				chargeFor.charge(user);
			}
			return;
		}
		
		cancel(false);
		warnUser(user, delay);
		initTimer((long)(delay * 1000.0), user, null, chargeFor, cause, true);
		teleTimer = ess.scheduleSyncRepeatingTask(this, 10, 10);
	}
	
	public void respawn(TeleportCause cause) throws Exception
	{
		final Player player = user.getBase();
		Location bed = player.getBedSpawnLocation();
		if (bed != null)
		{
			now(new Target(bed), cause);
		}
		else
		{
			if (ess.getSettings().isDebug())
			{
				ess.getLogger().info("Could not find bed spawn, forcing respawn event.");
			}
			final PlayerRespawnEvent pre = new PlayerRespawnEvent(player, player.getWorld().getSpawnLocation(), false);
			ess.getServer().getPluginManager().callEvent(pre);
			now(new Target(pre.getRespawnLocation()), cause);
		}
	}

	//The warp function is a wrapper used to teleport a player to a /warp
	public void warp(String warp, Trade chargeFor, TeleportCause cause) throws Exception
	{
		Location loc = ess.getWarps().getWarp(warp);
		user.sendMessage(_("warpingTo", warp));
		teleport(new Target(loc), chargeFor, cause);
	}

	//The back function is a wrapper used to teleport a player /back to their previous location.	
	public void back(Trade chargeFor) throws Exception
	{
		teleport(new Target(user.getLastLocation()), chargeFor, TeleportCause.COMMAND);
	}

	//This function is used to throw a user back after a jail sentence
	public void back() throws Exception
	{
		now(new Target(user.getLastLocation()), TeleportCause.COMMAND);
	}

	//This function handles teleporting to /home
	public void home(Location loc, Trade chargeFor) throws Exception
	{
		teleport(new Target(loc), chargeFor, TeleportCause.COMMAND);
	}
}
