content: /*
   *  ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
   *  Copyright (C) 2012 Kristian S. Stangeland
   *
   *  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 2 of 
   *  the License, or (at your option) any later version.
   *
   *  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.
   *
   *  You should have received a copy of the GNU General Public License along with this program; 
   *  if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 
   *  02111-1307 USA
   */
  
  package com.comphenix.protocol.events;
  
  import java.io.IOException;
  import java.io.ObjectInputStream;
  import java.io.ObjectOutputStream;
  import java.io.Serializable;
  import java.lang.reflect.Constructor;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.util.Map;
  import java.util.UUID;
  import java.util.concurrent.ConcurrentHashMap;
  
  import com.comphenix.protocol.reflect.accessors.Accessors;
  import com.comphenix.protocol.reflect.accessors.MethodAccessor;
  import com.comphenix.protocol.utility.ByteBuddyFactory;
  
  import org.bukkit.*;
  import org.bukkit.entity.EntityType;
  import org.bukkit.entity.Player;
  import org.bukkit.profile.PlayerProfile;
  
  import net.bytebuddy.description.ByteCodeElement;
  import net.bytebuddy.description.modifier.Visibility;
  import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
  import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
  import net.bytebuddy.implementation.FieldAccessor;
  import net.bytebuddy.implementation.InvocationHandlerAdapter;
  import net.bytebuddy.implementation.MethodCall;
  import net.bytebuddy.implementation.MethodDelegation;
  import net.bytebuddy.implementation.bind.annotation.AllArguments;
  import net.bytebuddy.implementation.bind.annotation.Origin;
  import net.bytebuddy.implementation.bind.annotation.FieldValue;
  import net.bytebuddy.implementation.bind.annotation.RuntimeType;
  import net.bytebuddy.matcher.ElementMatcher;
  import net.bytebuddy.matcher.ElementMatchers;
  
  /**
   * Represents a player object that can be serialized by Java.
   * 
   * @author Kristian
   */
  class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
  
  	/**
  	 * Generated by Eclipse.
  	 */
  	private static final long serialVersionUID = -2728976288470282810L;
  
  	private transient Location bedSpawnLocation;
  	
  	// Relevant data about an offline player
  	private String name;
  	private UUID uuid;
  	private long firstPlayed;
  	private long lastPlayed;
  	private boolean operator;
  	private boolean banned;
  	private boolean playedBefore;
  	private boolean online;
  	private boolean whitelisted;
  
  	private static final Constructor<?> proxyPlayerConstructor = setupProxyPlayerConstructor();
  
  	/**
  	 * Constructor used by serialization.
  	 */
  	public SerializedOfflinePlayer() {
  		// Do nothing
  	}
  	
  	/**
  	 * Initialize this serializable offline player from another player.
  	 * @param offline - another player.
  	 */
  	public SerializedOfflinePlayer(OfflinePlayer offline) {
  		this.name = offline.getName();
  		this.uuid = offline.getUniqueId();
  		this.firstPlayed = offline.getFirstPlayed();
  		this.lastPlayed = offline.getLastPlayed();
  		this.operator = offline.isOp();
  		this.banned = offline.isBanned();
  		this.playedBefore = offline.hasPlayedBefore();
  		this.online = offline.isOnline();
  		this.whitelisted = offline.isWhitelisted();
  	}
  	
  	@Override
  	public boolean isOp() {
  		return operator;
  	}
  
  	@Override
  	public void setOp(boolean operator) {
  		this.operator = operator;
  	}
  
  	@Override
  	public Map<String, Object> serialize() {
  		throw new UnsupportedOperationException();
  	}
  
  	@Override
  	public Location getBedSpawnLocation() {
  		return bedSpawnLocation;
  	}
  
  	// TODO do we need to implement this?
  	
  	public void incrementStatistic(Statistic statistic) throws IllegalArgumentException { }
  
  	public void decrementStatistic(Statistic statistic) throws IllegalArgumentException { }
  
  	public void incrementStatistic(Statistic statistic, int i) throws IllegalArgumentException { }
  
  	public void decrementStatistic(Statistic statistic, int i) throws IllegalArgumentException { }
  
  	public void setStatistic(Statistic statistic, int i) throws IllegalArgumentException { }
  
  	public int getStatistic(Statistic statistic) throws IllegalArgumentException {
  		return 0;
  	}
  
  	public void incrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { }
  
  	public void decrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { }
  
  	public int getStatistic(Statistic statistic, Material material) throws IllegalArgumentException {
  		return 0;
  	}
  
  	public void incrementStatistic(Statistic statistic, Material material, int i) throws IllegalArgumentException { }
  
  	public void decrementStatistic(Statistic statistic, Material material, int i) throws IllegalArgumentException { }
  
  	public void setStatistic(Statistic statistic, Material material, int i) throws IllegalArgumentException { }
  
  	public void incrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { }
  
  	public void decrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { }
  
  	public int getStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException {
  		return 0;
  	}
  
  	public void incrementStatistic(Statistic statistic, EntityType entityType, int i) throws IllegalArgumentException { }
  
  	public void decrementStatistic(Statistic statistic, EntityType entityType, int i) { }
  
  	public void setStatistic(Statistic statistic, EntityType entityType, int i) { }
  
  	@Override
  	public Location getLastDeathLocation() {
  		return null;
  	}
  
  	@Override
  	public long getFirstPlayed() {
  		return firstPlayed;
  	}
  
  	@Override
  	public long getLastPlayed() {
  		return lastPlayed;
  	}
  
  	@Override
  	public UUID getUniqueId() {
  		return uuid;
  	}
  
  	@Override
  	public PlayerProfile getPlayerProfile() {
  		return null;
  	}
  
  	@Override
  	public String getName() {
  		return name;
  	}
  	
  	@Override
  	public boolean hasPlayedBefore() {
  		return playedBefore;
  	}
  
  	@Override
  	public boolean isBanned() {
  		return banned;
  	}
  
  	public void setBanned(boolean banned) {
  		this.banned = banned;
  	}
  	
  	@Override
  	public boolean isOnline() {
  		return online;
  	}
  
  	@Override
  	public boolean isWhitelisted() {
  		return whitelisted;
  	}
  
  	@Override
  	public void setWhitelisted(boolean whitelisted) {
  		this.whitelisted = whitelisted;
  	}
  
  	private void writeObject(ObjectOutputStream output) throws IOException {
  		output.defaultWriteObject();
  		
  		// Serialize the bed spawn location
  		output.writeUTF(bedSpawnLocation.getWorld().getName());
  		output.writeDouble(bedSpawnLocation.getX());
  		output.writeDouble(bedSpawnLocation.getY());
  		output.writeDouble(bedSpawnLocation.getZ());
  	}
  	
  	private void readObject(ObjectInputStream input) throws ClassNotFoundException, IOException {
  		input.defaultReadObject();
  
  		// Well, this is a problem
  		bedSpawnLocation = new Location(
  				getWorld(input.readUTF()),
  				input.readDouble(),
  				input.readDouble(),
  				input.readDouble()
  		);
  	}
  	
  	private World getWorld(String name) {
  		try {
  			// Try to get the world at least
  			return Bukkit.getServer().getWorld(name);
  		} catch (Exception e) {
  			// Screw it
  			return null;
  		}
  	}
  	
  	@Override
  	public Player getPlayer() {
  		try {
  			// Try to get the real player underneath
  			return Bukkit.getServer().getPlayerExact(name);
  		} catch (Exception e) {
  			return getProxyPlayer();
  		}
  	}
  	
  	/**
  	 * Retrieve a player object that implements OfflinePlayer by referring to this object.
  	 * <p>
  	 * All other methods cause an exception.
  	 * @return Proxy object.
  	 */
  	public Player getProxyPlayer() {
  		try {
  			return (Player) proxyPlayerConstructor.newInstance(this);
  		} catch (IllegalAccessException e) {
  			throw new RuntimeException("Cannot access reflection.", e);
  		} catch (InstantiationException e) {
  			throw new RuntimeException("Cannot instantiate object.", e);
  		} catch (InvocationTargetException e) {
  			throw new RuntimeException("Error in invocation.", e);
  		}
  	}
  
  	private static Constructor<? extends Player> setupProxyPlayerConstructor()
  	{
  		final Method[] offlinePlayerMethods = OfflinePlayer.class.getMethods();
  		final String[] methodNames = new String[offlinePlayerMethods.length];
  		for (int idx = 0; idx < offlinePlayerMethods.length; ++idx)
  			methodNames[idx] = offlinePlayerMethods[idx].getName();
  
  		final Map<Method, MethodAccessor> accessorCache = new ConcurrentHashMap<>();
  		final ElementMatcher.Junction<ByteCodeElement> forwardedMethods = ElementMatchers.namedOneOf(methodNames);
  
  		try {
  			final MethodDelegation forwarding = MethodDelegation.withDefaultConfiguration().to(new Object() {
  				@RuntimeType
  				public Object intercept(
  						@Origin Method calledMethod,
  						@AllArguments Object[] args,
  						@FieldValue("offlinePlayer") OfflinePlayer proxy
  				) {
  					MethodAccessor accessor = accessorCache.computeIfAbsent(calledMethod, method -> {
  						// special case - some methods (like getName) are defined in OfflinePlayer as well
  						// as the online Player class. This causes cast exceptions if we try to invoke the method on
  						// the online player with our proxy. Prevent that
  						if (OfflinePlayer.class.isAssignableFrom(method.getDeclaringClass())) {
  							return Accessors.getMethodAccessor(
  									OfflinePlayer.class,
  									method.getName(),
  									method.getParameterTypes());
  						} else {
  							return Accessors.getMethodAccessor(method);
  						}
  					});
  					return accessor.invoke(proxy, args);
  				}
  			});
  
  			final InvocationHandlerAdapter throwException = InvocationHandlerAdapter.of((obj, method, args) -> {
  					throw new UnsupportedOperationException(
  						"The method " + method.getName() + " is not supported for offline players.");
  				});
  
  			return ByteBuddyFactory.getInstance()
  					.createSubclass(PlayerUnion.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
  					.name(SerializedOfflinePlayer.class.getPackage().getName() + ".PlayerInvocationHandler")
  
  					.defineField("offlinePlayer", OfflinePlayer.class, Visibility.PRIVATE)
  					.defineConstructor(Visibility.PUBLIC)
  					.withParameters(OfflinePlayer.class)
  					.intercept(MethodCall.invoke(Object.class.getDeclaredConstructor())
  							.andThen(FieldAccessor.ofField("offlinePlayer").setsArgumentAt(0)))
  
  					.method(forwardedMethods)
  					.intercept(forwarding)
  
  					.method(ElementMatchers.not(forwardedMethods))
  					.intercept(throwException)
  
  					.make()
  					.load(ByteBuddyFactory.getInstance().getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
  					.getLoaded()
  					.getDeclaredConstructor(OfflinePlayer.class);
  		} catch (NoSuchMethodException e) {
  			throw new RuntimeException("Failed to find Player constructor!", e);
  		}
  	}
  
  	/**
  	 * This interface extends both OfflinePlayer and Player (in that order) so that the class generated by ByteBuddy
  	 * looks at OfflinePlayer's methods first while still being a Player.
  	 */
  	private interface PlayerUnion extends OfflinePlayer, Player
  	{
  	}
  }
  