/*******************************************************************************
 * This file is part of ASkyBlock.
 *
 *     ASkyBlock 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.
 *
 *     ASkyBlock 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 ASkyBlock.  If not, see <http://www.gnu.org/licenses/>.
 *******************************************************************************/

package com.wasteofplastic.askyblock.schematics;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.UUID;

import org.bukkit.Material;
import org.bukkit.SkullType;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.Skull;

import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import com.wasteofplastic.org.jnbt.ByteTag;
import com.wasteofplastic.org.jnbt.CompoundTag;
import com.wasteofplastic.org.jnbt.IntTag;
import com.wasteofplastic.org.jnbt.ListTag;
import com.wasteofplastic.org.jnbt.StringTag;
import com.wasteofplastic.org.jnbt.Tag;

/**
 * This class describes skulls and is used in schematic importing
 * 
 * @author SpyL1nk
 * 
 */
public class SkullBlock {

    private static final Random random = new Random();
    private static final String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private SkullType skullType;
    private String skullOwnerName;
    private String skullOwnerUUID;
    private BlockFace skullRotation;
    private int skullStanding;
    private String skullTextureValue = null;
    private String skullTextureSignature = null;

    private static final Map<Integer, SkullType> skullTypeList = new HashMap<>();
    private static final Map<Integer, BlockFace> skullRotationList = new HashMap<>();

    static {
        skullTypeList.put(0, SkullType.SKELETON);
        skullTypeList.put(1, SkullType.WITHER);
        skullTypeList.put(2, SkullType.ZOMBIE);
        skullTypeList.put(3, SkullType.PLAYER);
        skullTypeList.put(4, SkullType.CREEPER);
    }

    static {
        skullRotationList.put(0, BlockFace.NORTH);
        skullRotationList.put(1, BlockFace.NORTH_NORTH_EAST);
        skullRotationList.put(2, BlockFace.NORTH_EAST);
        skullRotationList.put(3, BlockFace.EAST_NORTH_EAST);
        skullRotationList.put(4, BlockFace.EAST);
        skullRotationList.put(5, BlockFace.EAST_SOUTH_EAST);
        skullRotationList.put(6, BlockFace.SOUTH_EAST);
        skullRotationList.put(7, BlockFace.SOUTH_SOUTH_EAST);
        skullRotationList.put(8, BlockFace.SOUTH);
        skullRotationList.put(9, BlockFace.SOUTH_SOUTH_WEST);
        skullRotationList.put(10, BlockFace.SOUTH_WEST);
        skullRotationList.put(11, BlockFace.WEST_SOUTH_WEST);
        skullRotationList.put(12, BlockFace.WEST);
        skullRotationList.put(13, BlockFace.WEST_NORTH_WEST);
        skullRotationList.put(14, BlockFace.NORTH_WEST);
        skullRotationList.put(15, BlockFace.NORTH_NORTH_WEST);
    }

    @SuppressWarnings("deprecation")
    public boolean set(Block block) {
        Skull skull = (Skull) block.getState();
        if(skullOwnerName != null){
            skull.setOwner(skullOwnerName);
        }
        skull.setSkullType(skullType);
        skull.setRotation(skullRotation);
        skull.setRawData((byte) skullStanding);
        // Texture update
        if(skullTextureValue != null){
            setSkullWithNonPlayerProfile(skullTextureValue, skullTextureSignature, skullOwnerUUID, skullOwnerName, skull);
        }
        skull.update(true, false);
        return true;
    }

    public boolean prep(Map<String, Tag> tileData, int dataValue) {

        try {
            // Take skull type
            if(tileData.containsKey("SkullType")){
                int skullTypeId = (int) ((ByteTag) tileData.get("SkullType")).getValue();
                //Bukkit.getLogger().info("DEBUG: skull type = " + skullTypeId);
                if(skullTypeList.containsKey(skullTypeId)){
                    skullType = skullTypeList.get(skullTypeId);
                }
                else {
                    // Prevent hacks, set to default skull type
                    skullType = skullTypeList.get(0);
                }
            }
            else{
                // Prevent hacks, set to defaut skull type
                skullType = skullTypeList.get(0);
            }

            //Bukkit.getLogger().info("DEBUG: skull's data value = " + dataValue);
            // Data value 0 is actually unused for skulls, set to 2 to prevent hacks
            if(dataValue > 0 && dataValue < 6){

                skullStanding = dataValue;

                if(tileData.containsKey("Rot")){
                    int skullRotId = (int) ((ByteTag) tileData.get("Rot")).getValue();
                    //Bukkit.getLogger().info("DEBUG: skull's rotation byte = " + skullRotId);
                    // Useful for skulls on the floor to insert rotation data
                    if(skullRotationList.containsKey(skullStanding)){
                        skullRotation = skullRotationList.get(skullRotId);
                    }
                    else{
                        // Prevents hacks
                        skullRotation = skullRotationList.get(0);
                    }
                }
                else{
                    skullRotation = skullRotationList.get(0);
                }
            }
            else{

                skullStanding = 2;

                if(tileData.containsKey("Rot")){
                    int skullRotId = ((IntTag) tileData.get("Rot")).getValue();
                    //Bukkit.getLogger().info("DEBUG: skull's rotation byte = " + skullRotId);
                    // Useful for skulls on the floor to insert rotation data
                    if(skullRotationList.containsKey(skullStanding)){
                        skullRotation = skullRotationList.get(skullRotId);
                    }
                    // Prevents hacks
                    else{
                        skullRotation = skullRotationList.get(0);
                    }
                }
                else{
                    skullRotation = skullRotationList.get(0);
                }
            }

            // Check for Player Heads (skin, texture etc.)
            if(skullType == SkullType.PLAYER && tileData.containsKey("Owner")){

                Map<String, Tag> skullOwner = ((CompoundTag) tileData.get("Owner")).getValue();

                if(skullOwner.containsKey("Name")){
                    skullOwnerName = ((StringTag) skullOwner.get("Name")).getValue();

                    //Bukkit.getLogger().info("DEBUG: skull owner's name = " + skullOwnerName);
                }

                if(skullOwner.containsKey("Id")){
                    skullOwnerUUID = ((StringTag) skullOwner.get("Id")).getValue();

                    //Bukkit.getLogger().info("DEBUG: skull owner's UUID = " + skullOwnerUUID);
                }

                if(skullOwner.containsKey("Properties")){
                    Map<String, Tag> skullOwnerProperties = ((CompoundTag) skullOwner.get("Properties")).getValue();

                    if(skullOwnerProperties.containsKey("textures")){

                        ListTag listTagTextures = (ListTag) skullOwnerProperties.get("textures");

                        //Bukkit.getLogger().info("DEBUG: skull texture's list = " + listTagTextures);

                        if(listTagTextures != null){

                            // Logicaly, textures should have only one entry ...
                            Map<String, Tag> skullOwnerTextures = ((CompoundTag) listTagTextures.getValue().get(0)).getValue();

                            if(skullOwnerTextures.containsKey("Value")){
                                skullTextureValue = ((StringTag) skullOwnerTextures.get("Value")).getValue();

                                //Bukkit.getLogger().info("DEBUG: skull texture's value = " + skullTextureValue);
                            }
                            if(skullOwnerTextures.containsKey("Signature")){
                                skullTextureSignature = ((StringTag) skullOwnerTextures.get("Signature")).getValue();

                                //Bukkit.getLogger().info("DEBUG: skull's texture signature = " + skullTextureSignature);
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

    // Credits: GermanCoding
    @SuppressWarnings("deprecation")
    public static void setSkullWithNonPlayerProfile(String textureValue, String textureSignature, String ownerUUID, String ownerName, Skull skull) {
        if (skull.getType() != Material.SKULL)
            throw new IllegalArgumentException("Block must be a skull.");

        skull.getWorld().refreshChunk(skull.getChunk().getX(), skull.getChunk().getZ());


        // Difference beetween NonPlayerSkin and PlayerSkin
        if(textureSignature != null){
            try {
                setSkullProfile(skull, getPlayerProfile(textureValue, textureSignature, ownerUUID, ownerName));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (SecurityException e) {
                e.printStackTrace();
            }
        }
        else {
            try {
                setSkullProfile(skull, getNonPlayerProfile(textureValue, ownerUUID, ownerName));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (SecurityException e) {
                e.printStackTrace();
            }
        }

        //skull.getWorld().refreshChunk(skull.getChunk().getX(), skull.getChunk().getZ());
    }

    // Credits: val59000 (THANK YOU VERY MUCH VAL59000 !)
    private static void setSkullProfile(Skull skull, GameProfile gameProfile) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { 	

        Field profileField = null;
        try {
            profileField = skull.getClass().getDeclaredField("profile");
            profileField.setAccessible(true);
            profileField.set(skull, gameProfile);
        } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    // Credits: dori99xd
    public static GameProfile getNonPlayerProfile(String textureValue, String ownerUUID, String ownerName) {
        // Create a new GameProfile with .schematic informations or with fake informations
        GameProfile newSkinProfile = new GameProfile(ownerUUID == null ? UUID.randomUUID() : UUID.fromString(ownerUUID),
                ownerName == null ? getRandomString(16) : null);

        // Insert textures properties
        newSkinProfile.getProperties().put("textures", new Property("textures", textureValue));
        return newSkinProfile;
    }

    // Credits: dori99xd
    public static GameProfile getPlayerProfile(String textureValue, String textureSignature, String ownerUUID, String ownerName) {
        // Create a new GameProfile with .schematic informations or with fake informations
        GameProfile newSkinProfile = new GameProfile( ownerUUID == null ? UUID.randomUUID() : UUID.fromString(ownerUUID),
                ownerName == null ? getRandomString(16) : null);

        // Insert textures properties
        newSkinProfile.getProperties().put("textures", new Property("textures", textureValue, textureSignature));
        return newSkinProfile;
    }

    // Credits: dori99xd
    public static String getRandomString(int length) {
        StringBuilder b = new StringBuilder(length);
        for(int j = 0; j < length; j++){
            b.append(chars.charAt(random.nextInt(chars.length())));
        }
        return b.toString();
    }
}