package com.earth2me.essentials;

import java.util.ArrayList;
import org.bukkit.block.Block;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
/**
 * @author toi
 * Thanks to Raphfrk for optimization of this class.
 */
public class TargetBlock {

    private Location loc;
    private double viewHeight;
    private int maxDistance;
    private int[] blockToIgnore;
    private double checkDistance, curDistance;
    private double xRotation, yRotation;
    private Vector targetPos = new Vector();
    private Vector targetPosDouble = new Vector();
    private Vector prevPos = new Vector();
    private final Vector offset = new Vector();

    /**
     * Constructor requiring a player, uses default values
     * 
     * @param player Player to work with
     */
    public TargetBlock(Player player)
    {
        this.setValues(player.getLocation(), 300, 1.65, 0.2, null);
    }

    /**
     * Constructor requiring a location, uses default values
     * 
     * @param loc Location to work with
     */
    public TargetBlock(Location loc)
    {
        this.setValues(loc, 300, 0, 0.2, null);
    }

    /**
     * Constructor requiring a player, max distance and a checking distance
     * 
     * @param player Player to work with
     * @param maxDistance How far it checks for blocks
     * @param checkDistance How often to check for blocks, the smaller the more precise
     */
    public TargetBlock(Player player, int maxDistance, double checkDistance)
    {
        this.setValues(player.getLocation(), maxDistance, 1.65, checkDistance, null);
    }

    /**
     * Constructor requiring a location, max distance and a checking distance
     * 
     * @param loc What location to work with
     * @param maxDistance How far it checks for blocks
     * @param checkDistance How often to check for blocks, the smaller the more precise
     */
    public TargetBlock(Location loc, int maxDistance, double checkDistance) {
        this.setValues(loc, maxDistance, 0, checkDistance, null);
    }

    /**
     * Constructor requiring a player, max distance, checking distance and an array of blocks to ignore
     * 
     * @param player What player to work with
     * @param maxDistance How far it checks for blocks
     * @param checkDistance How often to check for blocks, the smaller the more precise
     * @param blocksToIgnore Integer array of what block ids to ignore while checking for viable targets
     */
    public TargetBlock (Player player, int maxDistance, double checkDistance, int[] blocksToIgnore)
    {
        this.setValues(player.getLocation(), maxDistance, 1.65, checkDistance, blocksToIgnore);
    }

    /**
     * Constructor requiring a location, max distance, checking distance and an array of blocks to ignore
     * 
     * @param loc What location to work with
     * @param maxDistance How far it checks for blocks
     * @param checkDistance How often to check for blocks, the smaller the more precise
     * @param blocksToIgnore Array of what block ids to ignore while checking for viable targets
     */
    public TargetBlock (Location loc, int maxDistance, double checkDistance, int[] blocksToIgnore)
    {
        this.setValues(loc, maxDistance, 0, checkDistance, blocksToIgnore);
    }

    /**
     * Constructor requiring a player, max distance, checking distance and an array of blocks to ignore
     * 
     * @param player What player to work with
     * @param maxDistance How far it checks for blocks
     * @param checkDistance How often to check for blocks, the smaller the more precise
     * @param blocksToIgnore String ArrayList of what block ids to ignore while checking for viable targets
     */
    public TargetBlock (Player player, int maxDistance, double checkDistance, ArrayList<String> blocksToIgnore)
    {
        int[] bti = this.convertStringArraytoIntArray(blocksToIgnore);
        this.setValues(player.getLocation(), maxDistance, 1.65, checkDistance, bti);
    }

    /**
     * Constructor requiring a location, max distance, checking distance and an array of blocks to ignore
     * 
     * @param loc What location to work with
     * @param maxDistance How far it checks for blocks
     * @param checkDistance How often to check for blocks, the smaller the more precise
     * @param blocksToIgnore String ArrayList of what block ids to ignore while checking for viable targets
     */
    public TargetBlock (Location loc, int maxDistance, double checkDistance, ArrayList<String> blocksToIgnore)
    {
        int[] bti = this.convertStringArraytoIntArray(blocksToIgnore);
        this.setValues(loc, maxDistance, 0, checkDistance, bti);
    }

    /**
     * Set the values, all constructors uses this function
     * 
     * @param loc Location of the view
     * @param maxDistance How far it checks for blocks
     * @param viewPos Where the view is positioned in y-axis
     * @param checkDistance How often to check for blocks, the smaller the more precise
     * @param blocksToIgnore Ids of blocks to ignore while checking for viable targets
     */
    private void setValues(Location loc, int maxDistance, double viewHeight, double checkDistance, int[] blocksToIgnore)
    {
        this.loc = loc;
        this.maxDistance = maxDistance;
        this.viewHeight = viewHeight;
        this.checkDistance = checkDistance;
        this.blockToIgnore = blocksToIgnore;
        this.curDistance = 0;
        xRotation = (loc.getYaw() + 90) % 360;
        yRotation = loc.getPitch() * -1;

        double h = (checkDistance * Math.cos(Math.toRadians(yRotation)));
        offset.setY((checkDistance * Math.sin(Math.toRadians(yRotation)))); 
        offset.setX((h * Math.cos(Math.toRadians(xRotation))));
        offset.setZ((h * Math.sin(Math.toRadians(xRotation))));

        targetPosDouble = new Vector(loc.getX(), loc.getY() + viewHeight, loc.getZ());
        targetPos = new Vector( targetPosDouble.getBlockX(), targetPosDouble.getBlockY(), targetPosDouble.getBlockZ());
        prevPos = targetPos.clone();
    }

    /**
     * Call this to reset checking position to allow you to check for a new target with the same TargetBlock instance.
     */
    public void reset()
    {
        targetPosDouble = new Vector(loc.getX(), loc.getY() + viewHeight, loc.getZ());
        targetPos = new Vector( targetPosDouble.getBlockX(), targetPosDouble.getBlockY(), targetPosDouble.getBlockZ());
        prevPos = targetPos.clone();
        this.curDistance = 0;
    }

    /**
     * Gets the distance to a block. Measures from the block underneath the player to the targetblock
     * Should only be used when passing player as an constructor parameter
     * 
     * @return double
     */
    public double getDistanceToBlock()
    {
        Vector blockUnderPlayer = new Vector(
                (int) Math.floor(loc.getX() + 0.5),
                (int) Math.floor(loc.getY() - 0.5),
                (int) Math.floor(loc.getZ() + 0.5));

        Block blk = getTargetBlock();
        double x = blk.getX() - blockUnderPlayer.getBlockX();
        double y = blk.getY() - blockUnderPlayer.getBlockY();
        double z = blk.getZ() - blockUnderPlayer.getBlockZ();

        return Math.sqrt((Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2)));
    }

    /**
     * Gets the rounded distance to a block. Measures from the block underneath the player to the targetblock
     * Should only be used when passing player as an constructor parameter
     * 
     * @return int
     */
    public int getDistanceToBlockRounded()
    {
        Vector blockUnderPlayer = new Vector(
                (int) Math.floor(loc.getX() + 0.5),
                (int) Math.floor(loc.getY() - 0.5),
                (int) Math.floor(loc.getZ() + 0.5));

        Block blk = getTargetBlock();
        double x = blk.getX() - blockUnderPlayer.getBlockX();
        double y = blk.getY() - blockUnderPlayer.getBlockY();
        double z = blk.getZ() - blockUnderPlayer.getBlockZ();

        return (int) Math.round((Math.sqrt((Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2)))));
    }

    /**
     * Gets the floored x distance to a block.
     * 
     * @return int
     */
    public int getXDistanceToBlock()
    {
        this.reset();
        return (int) Math.floor(getTargetBlock().getX() - loc.getBlockX() + 0.5);
    }

    /**
     * Gets the floored y distance to a block
     * 
     * @return int
     */
    public int getYDistanceToBlock()
    {
        this.reset();
        return (int) Math.floor(getTargetBlock().getY() - loc.getBlockY() + viewHeight);
    }

    /**
     * Gets the floored z distance to a block
     * 
     * @return int
     */
    public int getZDistanceToBlock()
    {
        this.reset();
        return (int) Math.floor(getTargetBlock().getZ() - loc.getBlockZ() + 0.5);
    }

    /**
     * Returns the block at the sight. Returns null if out of range or if no viable target was found
     * 
     * @return Block
     */
	@SuppressWarnings("empty-statement")
    public Block getTargetBlock()
    {
        this.reset();
        while ((getNextBlock() != null) && ((getCurrentBlock().getTypeId() == 0) || this.blockToIgnoreHasValue(getCurrentBlock().getTypeId())));
        return getCurrentBlock();
    }

    /**
     * Sets the type of the block at the sight. Returns false if the block wasn't set.
     * 
     * @param typeID ID of type to set the block to
     * @return boolean
     */
	@SuppressWarnings("empty-statement")
    public boolean setTargetBlock(int typeID)
    {
        if (Material.getMaterial(typeID) != null)
        {
            this.reset();
            while (getNextBlock() != null && getCurrentBlock().getTypeId() == 0);
            if (getCurrentBlock() != null)
            {
                Block blk = loc.getWorld().getBlockAt(targetPos.getBlockX(), targetPos.getBlockY(), targetPos.getBlockZ());
                blk.setTypeId(typeID);
                return true;
            }
        }
        return false;
    }

    /**
     * Sets the type of the block at the sight. Returns false if the block wasn't set.
     * 
     * @param type Material to set the block to
     * @return boolean
     */
	@SuppressWarnings("empty-statement")
    public boolean setTargetBlock(Material type)
    {
        this.reset();
        while ((getNextBlock() != null) && ((getCurrentBlock().getTypeId() == 0) || this.blockToIgnoreHasValue(getCurrentBlock().getTypeId())));
        if (getCurrentBlock() != null)
        {
            Block blk = loc.getWorld().getBlockAt(targetPos.getBlockX(), targetPos.getBlockY(), targetPos.getBlockZ());
            blk.setType(type);
            return true;
        }
        return false;
    }

    /**
     * Sets the type of the block at the sight. Returns false if the block wasn't set.
     * Observe! At the moment this function is using the built-in enumerator function .valueOf(String) but would preferably be changed to smarter function, when implemented
     * 
     * @param type Name of type to set the block to
     * @return boolean
     */
	@SuppressWarnings("empty-statement")
    public boolean setTargetBlock(String type)
    {
        Material mat = Material.valueOf(type);
        if (mat != null)
        {
            this.reset();
            while ((getNextBlock() != null) && ((getCurrentBlock().getTypeId() == 0) || this.blockToIgnoreHasValue(getCurrentBlock().getTypeId())));
            if (getCurrentBlock() != null)
            {
                Block blk = loc.getWorld().getBlockAt(targetPos.getBlockX(), targetPos.getBlockY(), targetPos.getBlockZ());
                blk.setType(mat);
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the block attached to the face at the sight. Returns null if out of range or if no viable target was found
     * 
     * @return Block
     */
	@SuppressWarnings("empty-statement")
    public Block getFaceBlock()
    {
        while ((getNextBlock() != null) && ((getCurrentBlock().getTypeId() == 0) || this.blockToIgnoreHasValue(getCurrentBlock().getTypeId())));
        if (getCurrentBlock() != null)
        {
            return getPreviousBlock();
        }
        else
        {
            return null;
        }
    }

    /**
     * Sets the type of the block attached to the face at the sight. Returns false if the block wasn't set.
     * 
     * @param typeID
     * @return boolean
     */
    public boolean setFaceBlock(int typeID)
    {
        if (Material.getMaterial(typeID) != null)
        {
            if (getCurrentBlock() != null)
            {
                Block blk = loc.getWorld().getBlockAt(prevPos.getBlockX(), prevPos.getBlockY(), prevPos.getBlockZ());
                blk.setTypeId(typeID);
                return true;
            }
        }
        return false;
    }

    /**
     * Sets the type of the block attached to the face at the sight. Returns false if the block wasn't set.
     * 
     * @param type
     * @return boolean
     */
    public boolean setFaceBlock(Material type)
    {
        if (getCurrentBlock() != null)
        {
            Block blk = loc.getWorld().getBlockAt(prevPos.getBlockX(), prevPos.getBlockY(), prevPos.getBlockZ());
            blk.setType(type);
            return true;
        }
        return false;
    }

    /**
     * Sets the type of the block attached to the face at the sight. Returns false if the block wasn't set.
     * Observe! At the moment this function is using the built-in enumerator function .valueOf(String) but would preferably be changed to smarter function, when implemented
     * 
     * @param type
     * @return boolean
     */
    public boolean setFaceBlock(String type)
    {
        Material mat = Material.valueOf(type);
        if (mat != null)
        {
            if (getCurrentBlock() != null)
            {
                Block blk = loc.getWorld().getBlockAt(prevPos.getBlockX(), prevPos.getBlockY(), prevPos.getBlockZ());
                blk.setType(mat);
                return true;
            }
        }
        return false;
    }

    /**
     * Get next block
     * 
     * @return Block
     */
    public Block getNextBlock()
    {
        prevPos = targetPos.clone();
        do
        {
            curDistance += checkDistance;

            targetPosDouble.setX(offset.getX() + targetPosDouble.getX());
            targetPosDouble.setY(offset.getY() + targetPosDouble.getY());
            targetPosDouble.setZ(offset.getZ() + targetPosDouble.getZ());
            targetPos = new Vector( targetPosDouble.getBlockX(), targetPosDouble.getBlockY(), targetPosDouble.getBlockZ());
        }
        while (curDistance <= maxDistance && targetPos.getBlockX() == prevPos.getBlockX() && targetPos.getBlockY() == prevPos.getBlockY() && targetPos.getBlockZ() == prevPos.getBlockZ());
        if (curDistance > maxDistance)
        {
            return null;
        }

        return this.loc.getWorld().getBlockAt(this.targetPos.getBlockX(), this.targetPos.getBlockY(), this.targetPos.getBlockZ());
    }

    /**
     * Returns the current block along the line of vision
     * 
     * @return Block
     */
    public Block getCurrentBlock()
    {
        if (curDistance > maxDistance)
        {
            return null;
        }
        else
        {
            return this.loc.getWorld().getBlockAt(this.targetPos.getBlockX(), this.targetPos.getBlockY(), this.targetPos.getBlockZ());
        }
    }

    /**
     * Sets current block type. Returns false if the block wasn't set.
     * 
     * @param typeID
     */
    public boolean setCurrentBlock(int typeID)
    {
        if (Material.getMaterial(typeID) != null)
        {
            Block blk = getCurrentBlock();
            if (blk != null)
            {
                blk.setTypeId(typeID);
                return true;
            }
        }
        return false;
    }

    /**
     * Sets current block type. Returns false if the block wasn't set.
     * 
     * @param type
     */
    public boolean setCurrentBlock(Material type)
    {
        Block blk = getCurrentBlock();
        if (blk != null)
        {
            blk.setType(type);
            return true;
        }
        return false;
    }

    /**
     * Sets current block type. Returns false if the block wasn't set.
     * Observe! At the moment this function is using the built-in enumerator function .valueOf(String) but would preferably be changed to smarter function, when implemented
     * 
     * @param type
     */
    public boolean setCurrentBlock(String type)
    {
        Material mat = Material.valueOf(type);
        if (mat != null)
        {
            Block blk = getCurrentBlock();
            if (blk != null)
            {
                blk.setType(mat);
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the previous block in the aimed path
     * 
     * @return Block
     */
    public Block getPreviousBlock()
    {
        return this.loc.getWorld().getBlockAt(prevPos.getBlockX(), prevPos.getBlockY(), prevPos.getBlockZ());
    }

    /**
     * Sets previous block type id. Returns false if the block wasn't set.
     * 
     * @param typeID
     */
    public boolean setPreviousBlock(int typeID)
    {
        if (Material.getMaterial(typeID) != null)
        {
            Block blk = getPreviousBlock(); 
            if (blk != null)
            {
                blk.setTypeId(typeID);
                return true;
            }
        }
        return false;
    }

    /**
     * Sets previous block type id. Returns false if the block wasn't set.
     * 
     * @param type
     */
    public boolean setPreviousBlock(Material type)
    {
        Block blk = getPreviousBlock(); 
        if (blk != null)
        {
            blk.setType(type);
            return true;
        }
        return false;
    }

    /**
     * Sets previous block type id. Returns false if the block wasn't set.
     * Observe! At the moment this function is using the built-in enumerator function .valueOf(String) but would preferably be changed to smarter function, when implemented
     * 
     * @param type
     */
    public boolean setPreviousBlock(String type)
    {
        Material mat = Material.valueOf(type);
        if (mat != null)
        {
            Block blk = getPreviousBlock(); 
            if (blk != null)
            {
                blk.setType(mat);
                return true;
            }
        }
        return false;
    }

    private int[] convertStringArraytoIntArray(ArrayList<String> array)
    {
        if (array != null)
        {
            int intarray[] = new int[array.size()];
            for (int i = 0; i < array.size(); i++)
            {
                try
                {
                    intarray[i] = Integer.parseInt(array.get(i));
                }
                catch (NumberFormatException nfe)
                {
                    intarray[i] = 0;
                }
            }
            return intarray;
        }
        return null;
    }

    private boolean blockToIgnoreHasValue(int value)
    {
        if (this.blockToIgnore != null)
        {
            if (this.blockToIgnore.length > 0)
            {
                for (int i : this.blockToIgnore)
                {
                    if (i == value)
                        return true;
                }
            }
        }
        return false;
    }
}