/**
 *   This file is part of Skript.
 *
 *  Skript 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  Skript 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 Skript.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Copyright 2011-2017 Peter Güttinger and contributors
 */
package ch.njol.skript.bukkitutil;

import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.UnsafeValues;
import org.bukkit.inventory.ItemStack;
import org.eclipse.jdt.annotation.Nullable;

import com.google.common.io.ByteStreams;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import ch.njol.skript.Skript;
import ch.njol.skript.util.Version;

/**
 * Contains helpers for Bukkit's not so safe stuff.
 */
@SuppressWarnings("deprecation")
public class BukkitUnsafe {
	
	/**
	 * Bukkit's UnsafeValues allows us to do stuff that would otherwise
	 * require NMS. It has existed for a long time, too, so 1.9 support is
	 * not particularly hard to achieve.
	 */
	private static final UnsafeValues unsafe;
	
	/**
	 * 1.9 Spigot has some "fun" bugs.
	 */
	private static final boolean knownNullPtr = !Skript.isRunningMinecraft(1, 11);
	
	static {
		UnsafeValues values = Bukkit.getUnsafe();
		if (values == null)
			throw new Error("UnsafeValues not available");
		unsafe = values;
	}
	
	/**
	 * Before 1.13, Vanilla material names were translated using
	 * this + a lookup table.
	 */
	@Nullable
	private static MethodHandle unsafeFromInternalNameMethod;
	
	private static final boolean newMaterials = Skript.isRunningMinecraft(1, 13);
	
	/**
	 * Vanilla material names to Bukkit materials.
	 */
	@Nullable
	private static Map<String,Material> materialMap;
	
	/**
	 * If we have material map for this version, using it is preferred.
	 * Otherwise, it can be used as fallback.
	 */
	private static boolean preferMaterialMap = true;
	
	/**
	 * We only spit one exception (unless debugging) from UnsafeValues. Some
	 * users might not care, and find 1.12 material mappings accurate enough.
	 */
	private static boolean unsafeValuesErrored;
	
	/**
	 * Maps pre 1.12 ids to materials for variable conversions.
	 */
	@Nullable
	private static Map<Integer,Material> idMappings;
	
	public static void initialize() {
		if (!newMaterials) {
			MethodHandle mh;
			try {
				mh = MethodHandles.lookup().findVirtual(UnsafeValues.class,
						"getMaterialFromInternalName", MethodType.methodType(Material.class, String.class));
			} catch (NoSuchMethodException | IllegalAccessException e) {
				mh = null;
			}
			unsafeFromInternalNameMethod = mh;
			
			try {
				Version version = Skript.getMinecraftVersion();
				boolean mapExists = loadMaterialMap("materials/" + version.getMajor() + "." +  version.getMinor() + ".json");
				if (!mapExists) {
					loadMaterialMap("materials/1.9.json"); // 1.9 is oldest we have mappings for
					preferMaterialMap = false;
					Skript.warning("Material mappings for " + version + " are not available.");
					Skript.warning("Depending on your server software, some aliases may not work.");
				}
			} catch (IOException e) {
				Skript.exception(e, "Failed to load material mappings. Aliases may not work properly.");
			}
		}
	}
	
	@Nullable
	public static Material getMaterialFromMinecraftId(String id) {
		if (newMaterials) {
			// On 1.13, Vanilla and Spigot names are same
			if (id.length() > 9)
				return Material.matchMaterial(id.substring(10)); // Strip 'minecraft:' out
			else // Malformed material name
				return null;
		} else {
			// If we have correct material map, prefer using it
			if (preferMaterialMap) {
				if (id.length() > 9) {
					assert materialMap != null;
					return materialMap.get(id.substring(10)); // Strip 'minecraft:' out
				}
			}
			
			// Otherwise, hacks
			Material type = null;
			try {
				assert unsafeFromInternalNameMethod != null;
				type = (Material) unsafeFromInternalNameMethod.invokeExact(unsafe, id);
			} catch (Throwable e) {
				// Only spit out an error once unless debugging
				if (!unsafeValuesErrored || Skript.debug()) {
					Skript.exception(e, "UnsafeValues failed to get material from Vanilla id");
					unsafeValuesErrored = true;
				}
			}
			if (type == null || type == Material.AIR) { // If there is no item form, UnsafeValues won't work
				// So we're going to rely on 1.12's material mappings
				assert materialMap != null;
				return materialMap.get(id);
			}
			return type;
		}
	}
	
	private static boolean loadMaterialMap(String name) throws IOException {
		try (InputStream is = Skript.getInstance().getResource(name)) {
			if (is == null) { // No mappings for this Minecraft version
				return false;
			}
			String data = new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8);
			
			Type type = new TypeToken<Map<String,Material>>(){}.getType();
			materialMap = new Gson().fromJson(data, type);
		}
		
		return true;
	}
	
	public static void modifyItemStack(ItemStack stack, String arguments) {
		try {
			unsafe.modifyItemStack(stack, arguments);
		} catch (NullPointerException e) {
			if (knownNullPtr) { // Probably known Spigot bug
				// So we continue doing whatever we were doing and hope it works
				Skript.warning("Item " + stack.getType() + arguments + " failed modifyItemStack. This is a bug on old Spigot versions.");
			} else { // Not known null pointer, don't just swallow
				throw e;
			}
		}
	}
	
	private static void initIdMappings() {
		try (InputStream is = Skript.getInstance().getResource("materials/ids.json")) {
			if (is == null) {
				throw new AssertionError("missing id mappings");
			}
			String data = new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8);
			
			Type type = new TypeToken<Map<Integer,String>>(){}.getType();
			Map<Integer, String> rawMappings = new Gson().fromJson(data, type);
			
			// Process raw mappings
			Map<Integer, Material> parsed = new HashMap<>(rawMappings.size());
			if (newMaterials) { // Legacy material conversion API
				for (Map.Entry<Integer, String> entry : rawMappings.entrySet()) {
					parsed.put(entry.getKey(), Material.matchMaterial(entry.getValue(), true));
				}
			} else { // Just enum API
				for (Map.Entry<Integer, String> entry : rawMappings.entrySet()) {
					parsed.put(entry.getKey(), Material.valueOf(entry.getValue()));
				}
			}
			idMappings = parsed;
		} catch (IOException e) {
			throw new AssertionError(e);
		}
	}
	
	@Nullable
	public static Material getMaterialFromId(int id) {
		if (idMappings == null) {
			initIdMappings();
		}
		assert idMappings != null;
		return idMappings.get(id);
	}
}