package tc.oc.pgm.renewable;

import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.material.MaterialData;
import org.bukkit.util.BlockVector;
import org.bukkit.geometry.Cuboid;
import org.bukkit.geometry.Vec3;
import tc.oc.commons.core.util.DefaultMapAdapter;

import java.util.HashMap;
import java.util.Map;

/**
 * Array-backed volume of block states (id:data pairs)
 * with fixed size and location. All positions in or out
 * are in world coordinates. Initially filled with air.
 */
public class BlockImage {
    private final World world;
    private final Vec3 origin;
    private final Vec3 size;
    private final Cuboid bounds;
    private final int volume;
    private final short[] blockIds;
    private final byte[] blockData;
    private final Map<MaterialData, Integer> blockCounts;

    public BlockImage(World world, Cuboid bounds) {
        this(world, bounds, false);
    }

    public BlockImage(World world, Cuboid bounds, boolean keepCounts) {
        this.world = world;
        this.bounds = bounds;
        this.origin = this.bounds.minimumBlockInside();
        this.size = this.bounds.blockSize();
        this.volume = Math.max(0, this.bounds.blockVolume());

        blockIds = new short[this.volume];
        blockData = new byte[this.volume];

        if(keepCounts) {
            this.blockCounts = new DefaultMapAdapter<>(new HashMap<MaterialData, Integer>(), 0);
        } else {
            this.blockCounts = null;
        }
    }

    /**
     * @return The boundaries of this image in world coordinates
     */
    public Cuboid getBounds() {
        return bounds;
    }

    public Map<MaterialData, Integer> getBlockCounts() {
        return blockCounts;
    }

    private int offset(BlockVector pos) {
        if(!this.bounds.containsBlock(pos)) {
            throw new IndexOutOfBoundsException("Block is not inside this BlockImage");
        }

        return (pos.coarseZ() - this.origin.coarseZ()) * this.size.coarseX() * this.size.coarseY() +
               (pos.coarseY() - this.origin.coarseY()) * this.size.coarseX() +
               (pos.coarseX() - this.origin.coarseX());
    }

    /**
     * @param pos   Block position in world coordinates
     * @return      Block state saved in this image at the given position
     */
    @SuppressWarnings("deprecation")
    public MaterialData get(BlockVector pos) {
        int offset = this.offset(pos);
        return new MaterialData(this.blockIds[offset], this.blockData[offset]);
    }

    @SuppressWarnings("deprecation")
    public BlockState getState(BlockVector pos) {
        int offset = this.offset(pos);
        BlockState state = pos.toLocation(this.world).getBlock().getState();
        state.setTypeId(this.blockIds[offset]);
        state.setRawData(this.blockData[offset]);
        return state;
    }

    /**
     * Set every block in this image to its current state in the world
     */
    @SuppressWarnings("deprecation")
    public void save() {
        if(this.blockCounts != null) {
            this.blockCounts.clear();
        }

        int offset = 0;
        for(Vec3 v : this.bounds.blockRegion().mutableIterable()) {
            Block block = this.world.getBlockAt(v.coarseX(),
                                                v.coarseY(),
                                                v.coarseZ());
            this.blockIds[offset] = (short) block.getTypeId();
            this.blockData[offset] = block.getData();
            ++offset;

            if(this.blockCounts != null) {
                MaterialData md = block.getState().getData();
                this.blockCounts.put(md, this.blockCounts.get(md) + 1);
            }
        }
    }

    /**
     * Copy the block at the given position from the image to the world
     * @param pos   Block position in world coordinates
     */
    @SuppressWarnings("deprecation")
    public void restore(BlockVector pos) {
        int offset = this.offset(pos);
        pos.toLocation(this.world).getBlock().setTypeIdAndData(this.blockIds[offset],
                                                               this.blockData[offset],
                                                               true);
    }
}