package org.spigotmc;

import gnu.trove.set.TByteSet;
import gnu.trove.set.hash.TByteHashSet;

public class AntiXray
{

    private static final CustomTimingsHandler update = new CustomTimingsHandler( "xray - update" );
    private static final CustomTimingsHandler obfuscate = new CustomTimingsHandler( "xray - obfuscate" );
    /*========================================================================*/
    // Used to keep track of which blocks to obfuscate
    private final boolean[] obfuscateBlocks = new boolean[ Short.MAX_VALUE ];
    // Used to select a random replacement ore
    private final byte[] replacementOres;

    public AntiXray(SpigotWorldConfig config)
    {
        // Set all listed blocks as true to be obfuscated
        for ( int id : ( config.engineMode == 1 ) ? config.hiddenBlocks : config.replaceBlocks )
        {
            obfuscateBlocks[id] = true;
        }

        // For every block
        TByteSet blocks = new TByteHashSet();
        for ( Integer i : config.hiddenBlocks )
        {
            net.minecraft.block.Block block = net.minecraft.block.Block.getBlockById( i );
            // Check it exists and is not a tile entity
            if ( block != null && !block.hasTileEntity() )
            {
                // Add it to the set of replacement blocks
                blocks.add( (byte) (int) i );
            }
        }
        // Bake it to a flat array of replacements
        replacementOres = blocks.toArray();
    }

    /**
     * Starts the timings handler, then updates all blocks within the set radius
     * of the given coordinate, revealing them if they are hidden ores.
     */
    public void updateNearbyBlocks(net.minecraft.world.World world, int x, int y, int z)
    {
        if ( world.getSpigotConfig().antiXray ) // Cauldron
        {
            update.startTiming();
            updateNearbyBlocks( world, x, y, z, 2, false ); // 2 is the radius, we shouldn't change it as that would make it exponentially slower
            update.stopTiming();
        }
    }

    /**
     * Removes all non exposed ores from the chunk buffer.
     */
    public void obfuscate(int chunkX, int chunkZ, int bitmask, byte[] buffer, net.minecraft.world.World world)
    {
        // If the world is marked as obfuscated
        if ( world.getSpigotConfig().antiXray ) // Cauldron
        {
            obfuscate.startTiming();
            // Initial radius to search around for air
            int initialRadius = 1;
            // Which block in the buffer we are looking at, anywhere from 0 to 16^4
            int index = 0;
            // The iterator marking which random ore we should use next
            int randomOre = 0;

            // Chunk corner X and Z blocks
            int startX = chunkX << 4;
            int startZ = chunkZ << 4;

            // Chunks can have up to 16 sections
            for ( int i = 0; i < 16; i++ )
            {
                // If the bitmask indicates this chunk is sent...
                if ( ( bitmask & 1 << i ) != 0 )
                {
                    // Work through all blocks in the chunk, y,z,x
                    for ( int y = 0; y < 16; y++ )
                    {
                        for ( int z = 0; z < 16; z++ )
                        {
                            for ( int x = 0; x < 16; x++ )
                            {
                                // For some reason we can get too far ahead of ourselves (concurrent modification on bulk chunks?) so if we do, just abort and move on
                                if ( index >= buffer.length )
                                {
                                    continue;
                                }
                                // Grab the block ID in the buffer.
                                // TODO: extended IDs are not yet supported
                                int blockId = buffer[index] & 0xFF;
                                // Check if the block should be obfuscated
                                if ( obfuscateBlocks[blockId] )
                                {
                                    // TODO: Don't really understand this, but if radius is not 0 and the world isn't loaded, bail out
                                    if ( initialRadius != 0 && !isLoaded( world, startX + x, ( i << 4 ) + y, startZ + z, initialRadius ) )
                                    {
                                        continue;
                                    }
                                    // On the otherhand, if radius is 0, or the nearby blocks are all non air, we can obfuscate
                                    if ( initialRadius == 0 || !hasTransparentBlockAdjacent( world, startX + x, ( i << 4 ) + y, startZ + z, initialRadius ) )
                                    {
                                        switch ( world.spigotConfig.engineMode )
                                        {
                                            case 1:
                                                // Replace with stone
                                                buffer[index] = 1;
                                                break;
                                            case 2:
                                                // Replace with random ore.
                                                if ( randomOre >= replacementOres.length )
                                                {
                                                    randomOre = 0;
                                                }
                                                buffer[index] = replacementOres[randomOre++];
                                                break;
                                        }
                                    }
                                }

                                index++;
                            }
                        }
                    }
                }
            }
            obfuscate.stopTiming();
        }
    }

    private void updateNearbyBlocks(net.minecraft.world.World world, int x, int y, int z, int radius, boolean updateSelf)
    {
        // If the block in question is loaded
        if ( world.blockExists( x, y, z ) )
        {
            // Get block id
            net.minecraft.block.Block block = world.getBlock( x, y, z );

            // See if it needs update
            if ( updateSelf && obfuscateBlocks[net.minecraft.block.Block.getIdFromBlock( block )] )
            {
                // Send the update
                world.markBlockForUpdate( x, y, z );
            }

            // Check other blocks for updates
            if ( radius > 0 )
            {
                updateNearbyBlocks( world, x + 1, y, z, radius - 1, true );
                updateNearbyBlocks( world, x - 1, y, z, radius - 1, true );
                updateNearbyBlocks( world, x, y + 1, z, radius - 1, true );
                updateNearbyBlocks( world, x, y - 1, z, radius - 1, true );
                updateNearbyBlocks( world, x, y, z + 1, radius - 1, true );
                updateNearbyBlocks( world, x, y, z - 1, radius - 1, true );
            }
        }
    }

    private static boolean isLoaded(net.minecraft.world.World world, int x, int y, int z, int radius)
    {
        return world.blockExists( x, y, z )
                || ( radius > 0
                && ( isLoaded( world, x + 1, y, z, radius - 1 )
                || isLoaded( world, x - 1, y, z, radius - 1 )
                || isLoaded( world, x, y + 1, z, radius - 1 )
                || isLoaded( world, x, y - 1, z, radius - 1 )
                || isLoaded( world, x, y, z + 1, radius - 1 )
                || isLoaded( world, x, y, z - 1, radius - 1 ) ) );
    }

    private static boolean hasTransparentBlockAdjacent(net.minecraft.world.World world, int x, int y, int z, int radius)
    {
        return !world.getBlock( x, y, z ).isNormalCube() /* isSolidBlock */
                || ( radius > 0
                && ( hasTransparentBlockAdjacent( world, x + 1, y, z, radius - 1 )
                || hasTransparentBlockAdjacent( world, x - 1, y, z, radius - 1 )
                || hasTransparentBlockAdjacent( world, x, y + 1, z, radius - 1 )
                || hasTransparentBlockAdjacent( world, x, y - 1, z, radius - 1 )
                || hasTransparentBlockAdjacent( world, x, y, z + 1, radius - 1 )
                || hasTransparentBlockAdjacent( world, x, y, z - 1, radius - 1 ) ) );
    }
}
