package net.glowstone.entity.meta;

import com.google.common.collect.ImmutableList;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import net.glowstone.util.DynamicallyTypedMapWithFloats;
import org.bukkit.entity.Entity;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.BlockVector;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * A map for entity metadata.
 */
@ToString(of = {"entityClass", "map"})
public class MetadataMap implements DynamicallyTypedMapWithFloats<MetadataIndex> {

    private final Map<MetadataIndex, Object> map = new EnumMap<>(MetadataIndex.class);
    private final List<Entry> changes = new ArrayList<>(4);
    private final Class<? extends Entity> entityClass;

    public MetadataMap(Class<? extends Entity> entityClass) {
        this.entityClass = entityClass;
        set(MetadataIndex.STATUS, 0);  // all entities have to have at least this
    }

    public boolean containsKey(MetadataIndex index) {
        return map.containsKey(index);
    }

    /**
     * Sets the value of a metadata field.
     *
     * @param index the field to set
     * @param value the new value
     */
    public void set(MetadataIndex index, Object value) {
        set(index, value, false);
    }

    /**
     * Sets the value of a metadata field.
     *
     * @param index the field to set
     * @param value the new value
     * @param force if the value should be forced as a change regardless of equality
     */
    public void set(MetadataIndex index, Object value, boolean force) {
        // take numbers down to the correct precision
        if (value != null) {
            if (value instanceof Number) {
                Number n = (Number) value;
                switch (index.getType()) {
                    case BYTE:
                        value = n.byteValue();
                        break;
                    case INT:
                        value = n.intValue();
                        break;
                    case FLOAT:
                        value = n.floatValue();
                        break;
                    default:
                        // do nothing
                }
            }
            if (!index.getType().getDataType().isAssignableFrom(value.getClass())) {
                throw new IllegalArgumentException(
                    "Cannot assign " + value + " to " + index + ", expects " + index.getType());
            }
            if (!index.appliesTo(entityClass)) {
                throw new IllegalArgumentException(
                    "Index " + index + " does not apply to " + entityClass.getSimpleName()
                        + ", only " + index.getAppliesTo().getSimpleName());
            }
        }

        Object prev = map.put(index, value);
        if (force || !Objects.equals(prev, value)) {
            changes.add(new Entry(index, value));
        }
    }

    public Object get(MetadataIndex index) {
        return map.get(index);
    }

    @SuppressWarnings("unchecked")
    private <T> T get(MetadataIndex index, MetadataType expected, T def) {
        if (index.getType() != expected) {
            throw new IllegalArgumentException(
                "Cannot get " + index + ": is " + index.getType() + ", not " + expected);
        }
        T t = (T) map.get(index);
        if (t == null) {
            return def;
        }
        return t;
    }

    public boolean getBit(MetadataIndex index, int bit) {
        return (getNumber(index).intValue() & bit) != 0;
    }

    /**
     * Sets or clears bits in an integer field.
     *
     * @param index the field to update
     * @param bit a mask of the bits to set or clear
     * @param status true to set; false to clear
     */
    public void setBit(MetadataIndex index, int bit, boolean status) {
        if (status) {
            set(index, getNumber(index).intValue() | bit);
        } else {
            set(index, getNumber(index).intValue() & ~bit);
        }
    }

    /**
     * Returns the numeric value of a metadata field.
     *
     * @param index the field to look up
     * @return the numeric value
     * @throws IllegalArgumentException if the value doesn't exist or isn't numeric
     */
    public Number getNumber(MetadataIndex index) {
        if (!containsKey(index)) {
            return 0;
        }
        Object o = get(index);
        if (!(o instanceof Number)) {
            throw new IllegalArgumentException(
                "Index " + index + " is of non-number type " + index.getType());
        }
        return (Number) o;
    }

    @Override
    public boolean getBoolean(MetadataIndex index) {
        return get(index, MetadataType.BOOLEAN, false);
    }

    public byte getByte(MetadataIndex index) {
        return get(index, MetadataType.BYTE, (byte) 0);
    }

    @Override
    public int getInt(MetadataIndex index) {
        return get(index, MetadataType.INT, 0);
    }

    @Override
    public float getFloat(MetadataIndex index) {
        return get(index, MetadataType.FLOAT, 0f);
    }

    @Override
    public String getString(MetadataIndex index) {
        return get(index, MetadataType.STRING, null);
    }

    public ItemStack getItem(MetadataIndex index) {
        return get(index, MetadataType.ITEM, null);
    }

    /**
     * Gets the optional position value for the given MetadataIndex.
     *
     * @param index the MetadataIndex of the optional position
     * @return the position value as a BlockVector, null if the value is not present
     */
    public BlockVector getOptPosition(MetadataIndex index) {
        return get(index, MetadataType.OPTPOSITION, null);
    }

    /**
     * Returns a list containing copies of all the entries.
     *
     * @return a list containing copies of all the entries
     */
    public List<Entry> getEntryList() {
        List<Entry> result = new ArrayList<>(map.size());
        result.addAll(
            map.entrySet().stream().map(entry -> new Entry(entry.getKey(), entry.getValue()))
                .collect(Collectors.toList()));
        Collections.sort(result);
        return result;
    }

    public List<Entry> getChanges() {
        Collections.sort(changes);
        return ImmutableList.copyOf(changes);
    }

    public void resetChanges() {
        changes.clear();
    }

    @RequiredArgsConstructor
    @EqualsAndHashCode
    public static class Entry implements Comparable<Entry> {

        public final MetadataIndex index;
        public final Object value;

        @Override
        public int compareTo(Entry o) {
            return o.index.getIndex() - index.getIndex();
        }

        @Override
        public String toString() {
            return index + "=" + value;
        }
    }
}

@@ -3554,32 +3554,190 @@
 eturn t;%0A    %7D%0A%0A
+    /**%0A     * Tests for the given bit mask in a bit field.%0A     *%0A     * @param index the field to test%0A     * @param bit a mask of the bits to test%0A     */%0A
     public boole
@@ -3797,39 +3797,26 @@
 urn (get
-Number(index).intValue(
+Byte(index
 ) & bit)
@@ -3872,17 +3872,12 @@
 in a
-n integer
+ bit
  fie
@@ -4156,39 +4156,26 @@
 dex, get
-Number(index).intValue(
+Byte(index
 ) %7C bit)
@@ -4223,31 +4223,18 @@
  get
-Number(index).intValue(
+Byte(index
 ) & 
@@ -4555,31 +4555,52 @@
     
-if (!containsKey(index)
+Object o = get(index);%0A        if (o == null
 ) %7B%0A
@@ -4635,39 +4635,8 @@
   %7D%0A
-        Object o = get(index);%0A
     

optimize bitfields in metadata map