package com.sk89q.worldedit.world.block;

import com.fastasyncworldedit.core.registry.state.PropertyKey;
import com.fastasyncworldedit.core.util.MathMan;
import com.fastasyncworldedit.core.world.block.BlockID;
import com.google.common.primitives.Booleans;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.extension.platform.Capability;
import com.sk89q.worldedit.extension.platform.Platform;
import com.sk89q.worldedit.registry.state.AbstractProperty;
import com.sk89q.worldedit.registry.state.Property;
import com.sk89q.worldedit.world.registry.BlockMaterial;
import com.sk89q.worldedit.world.registry.BlockRegistry;
import com.sk89q.worldedit.world.registry.Registries;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class BlockTypesCache {

    /*
     -----------------------------------------------------
                    Settings
     -----------------------------------------------------
     */
    protected static final class Settings {

        final int internalId;
        final BlockState defaultState;
        final AbstractProperty<?>[] propertiesMapArr;
        final AbstractProperty<?>[] propertiesArr;
        final List<AbstractProperty<?>> propertiesList;
        final Map<String, AbstractProperty<?>> propertiesMap;
        final Set<AbstractProperty<?>> propertiesSet;
        final BlockMaterial blockMaterial;
        final int permutations;
        int[] stateOrdinals;

        Settings(BlockType type, String id, int internalId, List<BlockState> states) {
            this.internalId = internalId;
            String propertyString = null;
            int propI = id.indexOf('[');
            if (propI != -1) {
                propertyString = id.substring(propI + 1, id.length() - 1);
            }

            int maxInternalStateId = 0;
            Map<String, ? extends Property<?>> properties = WorldEdit.getInstance().getPlatformManager().queryCapability(
                    Capability.GAME_HOOKS).getRegistries().getBlockRegistry().getProperties(type);
            if (!properties.isEmpty()) {
                // Ensure the properties are registered
                int maxOrdinal = 0;
                for (String key : properties.keySet()) {
                    maxOrdinal = Math.max(PropertyKey.getOrCreate(key).getId(), maxOrdinal);
                }
                this.propertiesMapArr = new AbstractProperty[maxOrdinal + 1];
                int prop_arr_i = 0;
                this.propertiesArr = new AbstractProperty[properties.size()];
                // Preserve properties order with LinkedHashMap
                HashMap<String, AbstractProperty<?>> propMap = new LinkedHashMap<>();

                int bitOffset = 0;
                for (Map.Entry<String, ? extends Property<?>> entry : properties.entrySet()) {
                    PropertyKey key = PropertyKey.getOrCreate(entry.getKey());
                    AbstractProperty<?> property = ((AbstractProperty) entry.getValue()).withOffset(bitOffset);
                    this.propertiesMapArr[key.getId()] = property;
                    this.propertiesArr[prop_arr_i++] = property;
                    propMap.put(entry.getKey(), property);

                    maxInternalStateId += (property.getValues().size() << bitOffset);
                    bitOffset += property.getNumBits();
                }
                this.propertiesList = Arrays.asList(this.propertiesArr);
                this.propertiesMap = Collections.unmodifiableMap(propMap);
                this.propertiesSet = new LinkedHashSet<>(this.propertiesMap.values());
            } else {
                this.propertiesMapArr = new AbstractProperty[0];
                this.propertiesArr = this.propertiesMapArr;
                this.propertiesList = Collections.emptyList();
                this.propertiesMap = Collections.emptyMap();
                this.propertiesSet = Collections.emptySet();
            }
            this.permutations = maxInternalStateId;

            this.blockMaterial = WorldEdit
                    .getInstance()
                    .getPlatformManager()
                    .queryCapability(Capability.GAME_HOOKS)
                    .getRegistries()
                    .getBlockRegistry()
                    .getMaterial(type);

            if (!propertiesList.isEmpty()) {
                this.stateOrdinals = generateStateOrdinals(internalId, states.size(), maxInternalStateId, propertiesList);

                for (int propId = 0; propId < this.stateOrdinals.length; propId++) {
                    int ordinal = this.stateOrdinals[propId];
                    if (ordinal != -1) {
                        int stateId = internalId + (propId << BIT_OFFSET);
                        CompoundTag defaultNBT = blockMaterial.getDefaultTile();
                        BlockState state = defaultNBT != null ? new BlockState(
                                type,
                                stateId,
                                ordinal,
                                blockMaterial.getDefaultTile()
                        ) :
                                new BlockState(type, stateId, ordinal);
                        states.add(state);
                    }
                }
                int defaultPropId = parseProperties(propertyString, propertiesMap) >> BIT_OFFSET;

                this.defaultState = states.get(this.stateOrdinals[defaultPropId]);
            } else {
                CompoundTag defaultNBT = blockMaterial.getDefaultTile();
                this.defaultState = defaultNBT != null ? new BlockState(
                        type,
                        internalId,
                        states.size(),
                        blockMaterial.getDefaultTile()
                ) :
                        new BlockState(type, internalId, states.size());
                states.add(this.defaultState);
            }
        }

        private int parseProperties(String properties, Map<String, AbstractProperty<?>> propertyMap) {
            int id = internalId;
            for (String keyPair : properties.split(",")) {
                String[] split = keyPair.split("=");
                String name = split[0];
                String value = split[1];
                AbstractProperty btp = propertyMap.get(name);
                id = btp.modify(id, btp.getValueFor(value));
            }
            return id;
        }

    }


    private static int[] generateStateOrdinals(int internalId, int ordinal, int maxStateId, List<AbstractProperty<?>> props) {
        if (props.isEmpty()) {
            return null;
        }
        int[] result = new int[maxStateId];
        Arrays.fill(result, -1);
        int[] state = new int[props.size()];
        int[] sizes = new int[props.size()];
        for (int i = 0; i < props.size(); i++) {
            sizes[i] = props.get(i).getValues().size();
        }
        int index = 0;
        outer:
        while (true) {
            // Create the state
            int stateId = internalId;
            for (int i = 0; i < state.length; i++) {
                stateId = props.get(i).modifyIndex(stateId, state[i]);
            }
            // Map it to the ordinal
            result[stateId >> BIT_OFFSET] = ordinal++;
            // Increment the state
            while (++state[index] == sizes[index]) {
                state[index] = 0;
                index++;
                if (index == state.length) {
                    break outer;
                }
            }
            index = 0;
        }
        return result;
    }

    /*
     -----------------------------------------------------
                    Static Initializer
     -----------------------------------------------------
     */

    public static final int BIT_OFFSET; // Used internally
    protected static final int BIT_MASK; // Used internally

    //private static final Map<String, BlockType> $REGISTRY = new HashMap<>();
    //public static final NamespacedRegistry<BlockType> REGISTRY = new NamespacedRegistry<>("block type", $REGISTRY);

    public static final BlockType[] values;
    public static final BlockState[] states;
    public static final boolean[] ticking;

    protected static final Set<String> $NAMESPACES = new LinkedHashSet<>();

    static {
        try {
            ArrayList<BlockState> stateList = new ArrayList<>();
            ArrayList<Boolean> tickList = new ArrayList<>();

            Platform platform = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.GAME_HOOKS);
            Registries registries = platform.getRegistries();
            BlockRegistry blockReg = registries.getBlockRegistry();
            Collection<String> blocks = blockReg.values();
            Map<String, String> blockMap = blocks.stream().collect(Collectors.toMap(item -> item.charAt(item.length() - 1) == ']'
                    ? item.substring(0, item.indexOf('['))
                    : item, item -> item));

            int size = blockMap.size() + 1;
            Field[] idFields = BlockID.class.getDeclaredFields();
            for (Field field : idFields) {
                size = Math.max(field.getInt(null) + 1, size);
            }
            BIT_OFFSET = MathMan.log2nlz(size);
            BIT_MASK = ((1 << BIT_OFFSET) - 1);
            values = new BlockType[size];

            // Register the statically declared ones first
            for (Field field : idFields) {
                if (field.getType() == int.class) {
                    int internalId = field.getInt(null);
                    String id = "minecraft:" + field.getName().toLowerCase(Locale.ROOT);
                    String defaultState = blockMap.remove(id);
                    if (defaultState == null) {
                        if (internalId != 0) {
                            continue;
                        }
                        defaultState = id;
                    }
                    if (values[internalId] != null) {
                        throw new IllegalStateException("Invalid duplicate id for " + field.getName());
                    }
                    BlockType type = register(defaultState, internalId, stateList, tickList);
                    // Note: Throws IndexOutOfBoundsError if nothing is registered and blocksMap is empty
                    values[internalId] = type;
                }
            }

            { // Register new blocks
                int internalId = 1;
                for (Map.Entry<String, String> entry : blockMap.entrySet()) {
                    String defaultState = entry.getValue();
                    // Skip already registered ids
                    for (; values[internalId] != null; internalId++) {
                    }
                    BlockType type = register(defaultState, internalId, stateList, tickList);
                    values[internalId] = type;
                }
            }
            for (int i = 0; i < values.length; i++) {
                if (values[i] == null) {
                    values[i] = values[0];
                }
            }

            states = stateList.toArray(new BlockState[stateList.size()]);
            ticking = Booleans.toArray(tickList);

        } catch (Throwable e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    private static BlockType register(final String id, int internalId, List<BlockState> states, List<Boolean> tickList) {
        // Get the enum name (remove namespace if minecraft:)
        int propStart = id.indexOf('[');
        String typeName = id.substring(0, propStart == -1 ? id.length() : propStart);
        String enumName = (typeName.startsWith("minecraft:") ? typeName.substring(10) : typeName).toUpperCase(Locale.ROOT);
        int oldsize = states.size();
        BlockType existing = new BlockType(id, internalId, states);
        tickList.addAll(Collections.nCopies(states.size() - oldsize, existing.getMaterial().isTicksRandomly()));
        // register states
        BlockType.REGISTRY.register(typeName, existing);
        String nameSpace = typeName.substring(0, typeName.indexOf(':'));
        $NAMESPACES.add(nameSpace);
        return existing;
    }

}
