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.injector;
  
  import com.comphenix.protocol.ProtocolLibrary;
  import com.comphenix.protocol.error.ErrorReporter;
  import com.comphenix.protocol.error.Report;
  import com.comphenix.protocol.error.ReportType;
  import com.comphenix.protocol.injector.PacketConstructor.Unwrapper;
  import com.comphenix.protocol.reflect.accessors.Accessors;
  import com.comphenix.protocol.reflect.accessors.FieldAccessor;
  import com.comphenix.protocol.reflect.instances.DefaultInstances;
  import com.comphenix.protocol.utility.MinecraftReflection;
  import com.google.common.primitives.Primitives;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.util.Collection;
  import java.util.Map;
  import java.util.concurrent.ConcurrentHashMap;
  import org.bukkit.Bukkit;
  import org.bukkit.entity.Player;
  
  /**
   * Represents an object capable of converting wrapped Bukkit objects into NMS objects.
   * <p>
   * Typical conversions include:
   * <ul>
   * <li>org.bukkit.entity.Player to net.minecraft.server.EntityPlayer</li>
   * <li>org.bukkit.World to net.minecraft.server.WorldServer</li>
   * </ul>
   *
   * @author Kristian
   */
  public class BukkitUnwrapper implements Unwrapper {
  
  	public static final ReportType REPORT_ILLEGAL_ARGUMENT = new ReportType("Illegal argument.");
  	public static final ReportType REPORT_SECURITY_LIMITATION = new ReportType("Security limitation.");
  	public static final ReportType REPORT_CANNOT_FIND_UNWRAP_METHOD = new ReportType("Cannot find method.");
  	public static final ReportType REPORT_CANNOT_READ_FIELD_HANDLE = new ReportType("Cannot read field 'handle'.");
  
  	private static final Map<Class<?>, Unwrapper> UNWRAPPER_CACHE = new ConcurrentHashMap<Class<?>, Unwrapper>();
  	private static BukkitUnwrapper DEFAULT;
  
  	// The current error reporter
  	private final ErrorReporter reporter;
  
  	/**
  	 * Construct a new Bukkit unwrapper with ProtocolLib's default error reporter.
  	 */
  	public BukkitUnwrapper() {
  		this(ProtocolLibrary.getErrorReporter());
  	}
  
  	/**
  	 * Construct a new Bukkit unwrapper with the given error reporter.
  	 *
  	 * @param reporter - the error reporter to use.
  	 */
  	public BukkitUnwrapper(ErrorReporter reporter) {
  		this.reporter = reporter;
  	}
  
  	/**
  	 * Retrieve the default instance of the Bukkit unwrapper.
  	 *
  	 * @return The default instance.
  	 */
  	public static BukkitUnwrapper getInstance() {
  		ErrorReporter currentReporter = ProtocolLibrary.getErrorReporter();
  
  		// Also recreate the unwrapper if the error reporter has changed
  		if (DEFAULT == null || DEFAULT.reporter != currentReporter) {
  			DEFAULT = new BukkitUnwrapper(currentReporter);
  		}
  		return DEFAULT;
  	}
  
  	private static Class<?> checkClass(Class<?> input, Class<?> expected, Class<?> result) {
  		if (expected.isAssignableFrom(input)) {
  			return result;
  		}
  		return null;
  	}
  
  	@SuppressWarnings("unchecked")
  	@Override
  	public Object unwrapItem(Object wrappedObject) {
  		// Special case
  		if (wrappedObject == null) {
  			return null;
  		}
  		Class<?> currentClass = PacketConstructor.getClass(wrappedObject);
  
  		// No need to unwrap primitives
  		if (currentClass.isPrimitive() || currentClass.equals(String.class)) {
  			return null;
  		}
  
  		// Next, check for types that doesn't have a getHandle()
  		if (wrappedObject instanceof Collection) {
  			return handleCollection((Collection<Object>) wrappedObject);
  		} else if (Primitives.isWrapperType(currentClass) || wrappedObject instanceof String) {
  			return null;
  		}
  
  		Unwrapper specificUnwrapper = getSpecificUnwrapper(currentClass);
  
  		// Retrieve the handle
  		if (specificUnwrapper != null) {
  			return specificUnwrapper.unwrapItem(wrappedObject);
  		} else {
  			return null;
  		}
  	}
  
  	// Handle a collection of items
  	private Object handleCollection(Collection<Object> wrappedObject) {
  
  		@SuppressWarnings("unchecked")
  		Collection<Object> copy = DefaultInstances.DEFAULT.getDefault(wrappedObject.getClass());
  
  		if (copy != null) {
  			// Unwrap every element
  			for (Object element : wrappedObject) {
  				copy.add(unwrapItem(element));
  			}
  			return copy;
  
  		} else {
  			// Impossible
  			return null;
  		}
  	}
  
  	/**
  	 * Retrieve a cached class unwrapper for the given class.
  	 *
  	 * @param type - the type of the class.
  	 * @return An unwrapper for the given class.
  	 */
  	private Unwrapper getSpecificUnwrapper(final Class<?> type) {
  		// See if we're already determined this
  		if (UNWRAPPER_CACHE.containsKey(type)) {
  			// We will never remove from the cache, so this ought to be thread safe
  			return UNWRAPPER_CACHE.get(type);
  		}
  
  		try {
  			final Method find = type.getMethod("getHandle");
  
  			// It's thread safe, as getMethod should return the same handle
  			Unwrapper methodUnwrapper = new Unwrapper() {
  				@Override
  				public Object unwrapItem(Object wrappedObject) {
  					try {
  						if (wrappedObject instanceof Class) {
  							return checkClass((Class<?>) wrappedObject, type, find.getReturnType());
  						}
  						return find.invoke(wrappedObject);
  
  					} catch (IllegalArgumentException e) {
  						reporter.reportDetailed(this,
  								Report.newBuilder(REPORT_ILLEGAL_ARGUMENT).error(e).callerParam(wrappedObject, find)
  						);
  					} catch (IllegalAccessException e) {
  						// Should not occur either
  						return null;
  					} catch (InvocationTargetException e) {
  						// This is really bad
  						throw new RuntimeException("Minecraft error.", e);
  					}
  
  					return null;
  				}
  			};
  
  			UNWRAPPER_CACHE.put(type, methodUnwrapper);
  			return methodUnwrapper;
  
  		} catch (SecurityException e) {
  			reporter.reportDetailed(this,
  					Report.newBuilder(REPORT_SECURITY_LIMITATION).error(e).callerParam(type)
  			);
  		} catch (NoSuchMethodException e) {
  			// Maybe it's a proxy?
  			Unwrapper proxyUnwrapper = getProxyUnwrapper(type);
  			if (proxyUnwrapper != null) {
  				return proxyUnwrapper;
  			}
  
  			// Try getting the field unwrapper too
  			Unwrapper fieldUnwrapper = getFieldUnwrapper(type);
  			if (fieldUnwrapper != null) {
  				return fieldUnwrapper;
  			} else {
  				reporter.reportDetailed(this,
  						Report.newBuilder(REPORT_CANNOT_FIND_UNWRAP_METHOD).error(e).callerParam(type));
  			}
  		}
  
  		// Default method
  		return null;
  	}
  
  	// Players should /always/ be able to be unwrapped
  	// We should only get here if the 'Player' is a proxy
  	private Unwrapper getProxyUnwrapper(final Class<?> type) {
  		try {
  			if (Player.class.isAssignableFrom(type)) {
  				final Method getHandle = MinecraftReflection.getCraftPlayerClass().getMethod("getHandle");
  
  				Unwrapper unwrapper = wrapped -> {
  					try {
  						return getHandle.invoke(((Player) wrapped).getPlayer());
  					} catch (Throwable ex) {
  						try {
  							return getHandle.invoke(Bukkit.getPlayer(((Player) wrapped).getUniqueId()));
  						} catch (ReflectiveOperationException ex1) {
  							throw new RuntimeException("Failed to unwrap proxy " + wrapped, ex);
  						}
  					}
  				};
  
  				UNWRAPPER_CACHE.put(type, unwrapper);
  				return unwrapper;
  			}
  		} catch (Throwable ignored) {
  		}
  
  		return null;
  	}
  
  	/**
  	 * Retrieve a cached unwrapper using the handle field.
  	 *
  	 * @param type - a cached field unwrapper.
  	 * @return The cached field unwrapper.
  	 */
  	private Unwrapper getFieldUnwrapper(final Class<?> type) {
  		// See if we succeeded
  		FieldAccessor accessor = Accessors.getFieldAccessorOrNull(type, "handle", null);
  		if (accessor != null) {
  			Unwrapper fieldUnwrapper = new Unwrapper() {
  				@Override
  				public Object unwrapItem(Object wrappedObject) {
  					try {
  						if (wrappedObject instanceof Class) {
  							return checkClass((Class<?>) wrappedObject, type, accessor.getField().getType());
  						}
  
  						return accessor.get(wrappedObject);
  					} catch (IllegalStateException e) {
  						reporter.reportDetailed(this,
  								Report.newBuilder(REPORT_CANNOT_READ_FIELD_HANDLE).error(e)
  										.callerParam(wrappedObject, accessor.getField())
  						);
  						return null;
  					}
  				}
  			};
  
  			UNWRAPPER_CACHE.put(type, fieldUnwrapper);
  			return fieldUnwrapper;
  
  		} else {
  			// Inform about this too
  			reporter.reportDetailed(this,
  					Report.newBuilder(REPORT_CANNOT_READ_FIELD_HANDLE).callerParam(type)
  			);
  			return null;
  		}
  	}
  }
  