import { contentLog, generateId, iterateChunk, Matrix, regionIterateBlocks, regionSize, regionTransformedBounds, regionVolume, Thread, Vector } from "./../../library/Minecraft.js";
import { Block, BlockPermutation, StructureMirrorAxis, StructureRotation, StructureSaveMode, world } from "@minecraft/server";
import { blockHasNBTData, locToString, stringToLoc, wrap } from "../util.js";
import { Jobs } from "./jobs.js";
export class RegionBuffer {
    static *create(start, end, func) {
        const min = Vector.min(start, end);
        const size = Vector.from(regionSize(start, end));
        const buffer = yield* this.saveStructs(undefined, start, end, (name, start, end) => world.structureManager.createEmpty(name, regionSize(start, end)));
        if (!buffer)
            return undefined;
        const volume = regionVolume(start, end);
        buffer.volume = volume;
        buffer.size = size;
        let i = 0;
        for (const loc of regionIterateBlocks(start, end)) {
            const localLoc = Vector.sub(loc, min);
            const block = func(localLoc);
            if (block)
                buffer.getBlock(localLoc).setPermutation(block instanceof BlockPermutation ? block : block.permutation);
            if (block instanceof Block && blockRecordable(block)) {
                const locString = locToString(localLoc);
                const name = buffer.id + "_block" + locString;
                world.structureManager.delete(name);
                buffer.extraBlockData[locString] = world.structureManager.createFromWorld(name, block.dimension, loc, loc, { includeEntities: false, saveMode: StructureSaveMode.Memory });
            }
            if (iterateChunk())
                yield Jobs.setProgress(i / volume);
            i++;
        }
        return buffer;
    }
    static *createFromWorld(start, end, dim, options = {}) {
        const min = Vector.min(start, end);
        const size = Vector.from(regionSize(start, end));
        const saveOptions = { includeEntities: options.includeEntities ?? false, saveMode: StructureSaveMode[options.saveAs ? "World" : "Memory"] };
        const buffer = yield* this.saveStructs(options.saveAs, start, end, (name, start, end) => world.structureManager.createFromWorld(name, dim, start, end, saveOptions));
        if (!buffer)
            return undefined;
        buffer.volume = regionVolume(start, end);
        buffer.size = size;
        if (options.recordBlocksWithData || options.modifier) {
            let i = 0;
            const volume = regionVolume(start, end);
            const modifier = options.modifier ?? (() => true);
            for (const loc of regionIterateBlocks(start, end)) {
                const block = dim.getBlock(loc) ?? (yield* Jobs.loadBlock(loc));
                const modResult = modifier(block);
                const localLoc = Vector.sub(loc, min);
                // Explicitly compare it to "true" since it could succeed with a block permutation
                if (modResult === true) {
                    if (options.recordBlocksWithData && blockRecordable(block)) {
                        const locString = locToString(localLoc);
                        const name = buffer.id + "_block" + locString;
                        world.structureManager.delete(name);
                        buffer.extraBlockData[locString] = world.structureManager.createFromWorld(name, dim, loc, loc, { includeEntities: false });
                    }
                }
                else {
                    buffer.getBlock(localLoc).setPermutation(!modResult ? undefined : modResult);
                }
                if (iterateChunk())
                    yield Jobs.setProgress(i / volume);
                i++;
            }
        }
        return buffer;
    }
    static get(name) {
        let structure;
        if ((structure = world.structureManager.get(name))) {
            const buffer = new RegionBuffer(name, false);
            buffer.structure = structure;
            buffer.volume = structure.size.x * structure.size.y * structure.size.z;
            buffer.size = Vector.from(structure.size);
            return buffer;
        }
        else if ((structure = world.structureManager.get(name + "_" + locToString(Vector.ZERO)))) {
            const maxIdx = Vector.ZERO;
            for (const axis of ["x", "y", "z"]) {
                while ((structure = world.structureManager.get(name + "_" + locToString(maxIdx))))
                    maxIdx[axis]++;
                maxIdx[axis]--;
            }
            const size = maxIdx.mul(this.MAX_SIZE).add(world.structureManager.get(name + "_" + locToString(maxIdx)).size);
            const buffer = new RegionBuffer(name, true);
            Array.from(Object.entries(this.getSubStructs(name, size))).forEach(([key, sub]) => (buffer.structures[key] = sub.structure));
            buffer.volume = size.x * size.y * size.z;
            buffer.size = size;
            return buffer;
        }
    }
    constructor(id, multipleStructures) {
        this.structures = {};
        this.extraBlockData = {};
        this.size = Vector.ZERO;
        this.volume = 0;
        this.refCount = 1;
        contentLog.debug("creating structure", this.id);
        this.id = id ?? "wedit:buffer_" + generateId();
        if (multipleStructures)
            this.getBlock = this.getBlockMulti;
        else
            this.getBlock = this.getBlockSingle;
    }
    *load(loc, dim, options = {}) {
        const rotation = options.rotation ?? Vector.ZERO;
        const flip = options.flip ?? Vector.ONE;
        const bounds = this.getBounds(loc, options);
        const matrix = RegionBuffer.getTransformationMatrix(loc, options);
        const invMatrix = matrix.invert();
        const shouldTransform = options.rotation || options.flip;
        let transform;
        if (shouldTransform) {
            transform = (block) => {
                const blockName = block.type.id;
                const attachment = block.getState("attachment");
                const direction = block.getState("direction");
                const doorHingeBit = block.getState("door_hinge_bit");
                const facingDir = block.getState("facing_direction");
                const groundSignDir = block.getState("ground_sign_direction");
                const openBit = block.getState("open_bit");
                const pillarAxis = block.getState("pillar_axis");
                const topSlotBit = block.getState("top_slot_bit");
                const upsideDownBit = block.getState("upside_down_bit");
                const weirdoDir = block.getState("weirdo_direction");
                const torchFacingDir = block.getState("torch_facing_direction");
                const leverDir = block.getState("lever_direction");
                const cardinalDir = block.getState("minecraft:cardinal_direction");
                const withProperties = (properties) => {
                    for (const prop in properties)
                        block = block.withState(prop, properties[prop]);
                    return block;
                };
                if (upsideDownBit != null && openBit != null && direction != null) {
                    const states = this.transformMapping(mappings.trapdoorMap, `${upsideDownBit}_${openBit}_${direction}`, matrix).split("_");
                    block = withProperties({ upside_down_bit: states[0] == "true", open_bit: states[1] == "true", direction: parseInt(states[2]) });
                }
                else if (weirdoDir != null && upsideDownBit != null) {
                    const states = this.transformMapping(mappings.stairsMap, `${upsideDownBit}_${weirdoDir}`, matrix).split("_");
                    block = withProperties({ upside_down_bit: states[0] == "true", weirdo_direction: parseInt(states[1]) });
                }
                else if (doorHingeBit != null && direction != null) {
                    const states = this.transformMapping(mappings.doorMap, `${doorHingeBit}_${direction}`, matrix).split("_");
                    block = withProperties({ door_hinge_bit: states[0] == "true", direction: parseInt(states[1]) });
                }
                else if (attachment != null && direction != null) {
                    const states = this.transformMapping(mappings.bellMap, `${attachment}_${direction}`, matrix).split("_");
                    block = withProperties({ attachment: states[0], direction: parseInt(states[1]) });
                }
                else if (cardinalDir != null) {
                    const state = this.transformMapping(mappings.cardinalDirectionMap, cardinalDir, matrix);
                    block = block.withState("minecraft:cardinal_direction", state);
                }
                else if (facingDir != null) {
                    const state = this.transformMapping(mappings.facingDirectionMap, facingDir, matrix);
                    block = block.withState("facing_direction", parseInt(state));
                }
                else if (direction != null) {
                    const mapping = blockName.includes("powered_repeater") || blockName.includes("powered_comparator") ? mappings.redstoneMap : mappings.directionMap;
                    const state = this.transformMapping(mapping, direction, matrix);
                    block = block.withState("direction", parseInt(state));
                }
                else if (groundSignDir != null) {
                    const state = this.transformMapping(mappings.groundSignDirectionMap, groundSignDir, matrix);
                    block = block.withState("ground_sign_direction", parseInt(state));
                }
                else if (torchFacingDir != null) {
                    const state = this.transformMapping(mappings.torchMap, torchFacingDir, matrix);
                    block = block.withState("torch_facing_direction", state);
                }
                else if (leverDir != null) {
                    const state = this.transformMapping(mappings.leverMap, leverDir, matrix);
                    block = block.withState("lever_direction", state.replace("0", ""));
                }
                else if (pillarAxis != null) {
                    const state = this.transformMapping(mappings.pillarAxisMap, pillarAxis + "_0", matrix);
                    block = block.withState("pillar_axis", state[0]);
                }
                else if (topSlotBit != null) {
                    const state = this.transformMapping(mappings.topSlotMap, String(topSlotBit), matrix);
                    block = block.withState("top_slot_bit", state == "true");
                }
                return block;
            };
        }
        else {
            transform = (block) => block;
        }
        if ((Math.abs(rotation.y) / 90) % 1 != 0 || rotation.x || rotation.z || flip.y != 1 || options.mask) {
            let i = 0;
            const totalIterationCount = regionVolume(...bounds);
            for (const blockLoc of regionIterateBlocks(...bounds)) {
                const sample = Vector.from(blockLoc).add(0.5).transform(invMatrix).floor();
                const block = this.getBlock(sample);
                if (iterateChunk())
                    yield Jobs.setProgress(i / totalIterationCount);
                i++;
                if (!block?.permutation)
                    continue;
                let oldBlock = dim.getBlock(blockLoc);
                if (!oldBlock && Jobs.inContext())
                    oldBlock = yield* Jobs.loadBlock(blockLoc);
                if (options.mask && !options.mask.matchesBlock(oldBlock))
                    continue;
                if (block.nbtStructure)
                    world.structureManager.place(block.nbtStructure, dim, blockLoc);
                oldBlock.setPermutation(transform(block.permutation));
            }
            const volumeQuery = { location: loc, volume: Vector.sub(this.size, [1, 1, 1]) };
            const oldEntities = dim.getEntities(volumeQuery);
            yield* this.loadStructs(loc, dim, { includeBlocks: false });
            if (shouldTransform) {
                dim.getEntities(volumeQuery)
                    .filter((entity) => !oldEntities.some((old) => old.id === entity.id))
                    .forEach((entity) => {
                    let location = entity.location;
                    let facingLocation = Vector.add(entity.getViewDirection(), location);
                    location = Vector.from(location).sub(loc).transform(matrix).add(loc);
                    facingLocation = Vector.from(facingLocation).sub(loc).transform(matrix).add(loc);
                    entity.teleport(location, { dimension: dim, facingLocation });
                });
            }
        }
        else {
            yield* this.loadStructs(bounds[0], dim, { rotation: rotation.y, flip });
            let i = 0;
            const totalIterationCount = Object.keys(this.extraBlockData).length;
            for (const key in this.extraBlockData) {
                let blockLoc = stringToLoc(key);
                blockLoc = (shouldTransform ? Vector.add(blockLoc, 0.5).transform(matrix) : Vector.add(blockLoc, loc)).floor();
                if (iterateChunk())
                    yield Jobs.setProgress(i / totalIterationCount);
                i++;
                let oldBlock = dim.getBlock(blockLoc);
                if (!oldBlock && Jobs.inContext())
                    oldBlock = yield* Jobs.loadBlock(blockLoc);
                world.structureManager.place(this.extraBlockData[key], dim, blockLoc);
                oldBlock.setPermutation(transform(oldBlock.permutation));
            }
        }
    }
    getSize() {
        return this.size;
    }
    getBounds(loc, options = {}) {
        return RegionBuffer.createBounds(loc, Vector.add(loc, this.size).sub(1), options);
    }
    getVolume() {
        return this.volume;
    }
    *getBlocks() {
        for (const loc of regionIterateBlocks(Vector.ZERO, Vector.sub(this.size, [1, 1, 1])))
            yield this.getBlock(loc);
    }
    ref() {
        this.refCount++;
    }
    deref() {
        if (--this.refCount < 1)
            this.delete();
    }
    getBlockSingle(loc) {
        if (loc.x < 0 || loc.x >= this.size.x || loc.y < 0 || loc.y >= this.size.y || loc.z < 0 || loc.z >= this.size.z)
            return undefined;
        return new RegionBlockImpl(this, this.extraBlockData, loc, this.structure, loc);
    }
    getBlockMulti(loc) {
        if (loc.x < 0 || loc.x >= this.size.x || loc.y < 0 || loc.y >= this.size.y || loc.z < 0 || loc.z >= this.size.z)
            return undefined;
        const offset = { x: Math.floor(loc.x / RegionBuffer.MAX_SIZE.x), y: Math.floor(loc.y / RegionBuffer.MAX_SIZE.y), z: Math.floor(loc.z / RegionBuffer.MAX_SIZE.z) };
        const structure = this.structures[locToString(offset)];
        return new RegionBlockImpl(this, this.extraBlockData, loc, structure, Vector.sub(loc, Vector.mul(offset, RegionBuffer.MAX_SIZE)));
    }
    *loadStructs(loc, dim, options = {}) {
        const loadPos = Vector.from(loc);
        const rotation = new Vector(0, options.rotation ?? 0, 0);
        const mirror = new Vector(Math.sign(options.flip?.x ?? 1), 1, Math.sign(options.flip?.z ?? 1));
        const loadOptions = {
            rotation: {
                "0": StructureRotation.None,
                "1": StructureRotation.Rotate90,
                "2": StructureRotation.Rotate180,
                "3": StructureRotation.Rotate270,
            }[wrap((rotation.y ?? 0) / 90, 4)],
            mirror: {
                "1 1": StructureMirrorAxis.None,
                "-1 1": StructureMirrorAxis.Z,
                "1 -1": StructureMirrorAxis.X,
                "-1 -1": StructureMirrorAxis.XZ,
            }[`${mirror.x} ${mirror.z}`],
            includeBlocks: options.includeBlocks ?? true,
        };
        if (!this.structure) {
            const size = this.size;
            const transform = Matrix.fromRotationFlipOffset(rotation, mirror);
            const bounds = regionTransformedBounds(Vector.ZERO, size.sub(1).floor(), transform);
            let error = false;
            for (const [key, structure] of Object.entries(this.structures)) {
                const offset = stringToLoc(key).mul(RegionBuffer.MAX_SIZE);
                const subBounds = regionTransformedBounds(offset, offset.add(structure.size).sub(1), transform);
                const subStart = Vector.sub(subBounds[0], bounds[0]).add(loadPos);
                const subEnd = Vector.sub(subBounds[1], bounds[0]).add(loadPos);
                yield* Jobs.loadArea(subStart, subEnd);
                try {
                    world.structureManager.place(structure, dim, subStart, loadOptions);
                }
                catch {
                    error = true;
                    break;
                }
            }
            return error;
        }
        else {
            yield* Jobs.loadArea(loc, Vector.add(loc, this.size).sub(1));
            try {
                world.structureManager.place(this.structure, dim, loadPos.floor(), loadOptions);
                return false;
            }
            catch {
                return true;
            }
        }
    }
    transformMapping(mapping, state, transform) {
        let vec = Vector.from(mapping[state]);
        if (!vec) {
            contentLog.debug(`Can't map state "${state}".`);
            return typeof state == "string" ? state : state.toString();
        }
        vec = vec.transformDirection(transform);
        let closestState;
        let closestDot = -1000;
        for (const newState in mapping) {
            const dot = Vector.from(mapping[newState]).dot(vec);
            if (dot > closestDot) {
                closestState = newState;
                closestDot = dot;
            }
        }
        return closestState;
    }
    delete() {
        const thread = new Thread();
        thread.start(function* (self) {
            for (const structure of Object.values(self.extraBlockData))
                world.structureManager.delete(structure), yield;
            for (const structure of Object.values(self.structures))
                world.structureManager.delete(structure), yield;
            if (self.structure)
                world.structureManager.delete(self.structure);
            self.size = Vector.ZERO;
            self.volume = 0;
            contentLog.debug("deleted structure", self.id);
        }, this);
    }
    static createBounds(start, end, options = {}) {
        return regionTransformedBounds(Vector.ZERO, Vector.sub(end, start).floor(), RegionBuffer.getTransformationMatrix(start, options));
    }
    static getTransformationMatrix(loc, options = {}) {
        const offset = Matrix.fromTranslation(options.offset ?? Vector.ZERO);
        return Matrix.fromRotationFlipOffset(options.rotation ?? Vector.ZERO, options.flip ?? Vector.ONE)
            .multiply(offset)
            .translate(loc);
    }
    static *saveStructs(name, start, end, createFunc) {
        const min = Vector.min(start, end);
        const size = regionSize(start, end);
        if (RegionBuffer.beyondMaxSize(size)) {
            let error = false;
            const buffer = new RegionBuffer(name, true);
            for (const [key, sub] of Object.entries(this.getSubStructs(buffer.id, size))) {
                const subStart = min.add(sub.start);
                const subEnd = min.add(sub.end);
                yield* Jobs.loadArea(subStart, subEnd);
                try {
                    world.structureManager.delete(sub.name);
                    sub.structure = createFunc(sub.name, min.add(sub.start), min.add(sub.end));
                    buffer.structures[key] = sub.structure;
                }
                catch {
                    error = true;
                    break;
                }
            }
            if (error) {
                Object.values(buffer.structures).forEach((struct) => world.structureManager.delete(struct));
                return;
            }
            else {
                return buffer;
            }
        }
        else {
            const buffer = new RegionBuffer(name, false);
            yield* Jobs.loadArea(start, end);
            try {
                world.structureManager.delete(buffer.id);
                buffer.structure = createFunc(buffer.id, start, end);
                return buffer;
            }
            catch {
                return;
            }
        }
    }
    static beyondMaxSize(size) {
        return size.x > RegionBuffer.MAX_SIZE.x || size.y > RegionBuffer.MAX_SIZE.y || size.z > RegionBuffer.MAX_SIZE.z;
    }
    static getSubStructs(name, size) {
        const subStructs = {};
        for (let z = 0; z < size.z; z += this.MAX_SIZE.z)
            for (let y = 0; y < size.y; y += this.MAX_SIZE.y)
                for (let x = 0; x < size.x; x += this.MAX_SIZE.x) {
                    const subStart = new Vector(x, y, z);
                    const subEnd = Vector.min(subStart.add(this.MAX_SIZE).sub(1), size.sub(1));
                    const locString = `${x / this.MAX_SIZE.x}_${y / this.MAX_SIZE.y}_${z / this.MAX_SIZE.z}`;
                    subStructs[locString] = {
                        structure: world.structureManager.get(name + "_" + locString),
                        name: name + "_" + locString,
                        start: subStart,
                        end: subEnd,
                    };
                }
        return subStructs;
    }
}
RegionBuffer.MAX_SIZE = new Vector(64, 256, 64);
class RegionBlockImpl {
    constructor(buffer, extraBlockData, location, structure, inStructureLocation) {
        this.buffer = buffer;
        this.x = Math.floor(location.x);
        this.y = Math.floor(location.y);
        this.z = Math.floor(location.z);
        this.bufferBlockNBT = extraBlockData;
        this.bufferStructure = structure;
        this.bufferStructureLocation = inStructureLocation;
    }
    get permutation() {
        return this.bufferStructure.getBlockPermutation(this.bufferStructureLocation);
    }
    get location() {
        return { x: this.x, y: this.y, z: this.z };
    }
    get nbtStructure() {
        return this.bufferBlockNBT[locToString(this.location)];
    }
    get type() {
        return this.permutation.type;
    }
    get typeId() {
        return this.permutation.type.id;
    }
    get isAir() {
        return this.permutation.matches(RegionBlockImpl.AIR);
    }
    get isLiquid() {
        return RegionBlockImpl.LIQUIDS.includes(this.permutation.type.id);
    }
    get isWaterlogged() {
        return this.bufferStructure.getIsWaterlogged(this.bufferStructureLocation);
    }
    above(steps) {
        return this.buffer.getBlock({ x: this.x, y: this.y + (steps ?? 1), z: this.z });
    }
    below(steps) {
        return this.buffer.getBlock({ x: this.x, y: this.y - (steps ?? 1), z: this.z });
    }
    north(steps) {
        return this.buffer.getBlock({ x: this.x, y: this.y, z: this.z - (steps ?? 1) });
    }
    south(steps) {
        return this.buffer.getBlock({ x: this.x, y: this.y, z: this.z + (steps ?? 1) });
    }
    east(steps) {
        return this.buffer.getBlock({ x: this.x + (steps ?? 1), y: this.y, z: this.z });
    }
    west(steps) {
        return this.buffer.getBlock({ x: this.x - (steps ?? 1), y: this.y, z: this.z });
    }
    offset(offset) {
        return this.buffer.getBlock({ x: this.x + offset.x, y: this.y + offset.y, z: this.z + offset.z });
    }
    bottomCenter() {
        return { x: this.x + 0.5, y: this.y, z: this.z + 0.5 };
    }
    center() {
        return { x: this.x + 0.5, y: this.y + 0.5, z: this.z + 0.5 };
    }
    getTags() {
        return this.permutation.getTags();
    }
    hasTag(tag) {
        return this.permutation.hasTag(tag);
    }
    matches(blockName, states) {
        return this.permutation.matches(blockName, states);
    }
    setPermutation(permutation) {
        let key;
        if (permutation?.type.id !== this.permutation?.type.id && (key = locToString(this.location)) in this.bufferBlockNBT) {
            world.structureManager.delete(this.bufferBlockNBT[key]);
            delete this.bufferBlockNBT[key];
        }
        this.bufferStructure.setBlockPermutation(this.bufferStructureLocation, permutation);
    }
    setType(blockType) {
        this.setPermutation(BlockPermutation.resolve(typeof blockType === "string" ? blockType : blockType.id));
    }
}
RegionBlockImpl.AIR = "minecraft:air";
RegionBlockImpl.LIQUIDS = ["minecraft:water", "minecraft:flowing_water", "minecraft:lava", "minecraft:flowing_lava"];
function blockRecordable(block) {
    return blockHasNBTData(block) || /* Until Mojang fixes trapdoor rotation... */ block.typeId.match(/^minecraft:.*trapdoor$/);
}
const mappings = {
    topSlotMap: {
        // upside_down_bit
        false: new Vector(0, 1, 0),
        true: new Vector(0, -1, 0),
    },
    redstoneMap: {
        0: new Vector(0, 0, -1),
        1: new Vector(1, 0, 0),
        2: new Vector(0, 0, 1),
        3: new Vector(-1, 0, 0),
    },
    directionMap: {
        // direction
        0: new Vector(1, 0, 0),
        1: new Vector(0, 0, 1),
        2: new Vector(-1, 0, 0),
        3: new Vector(0, 0, -1),
    },
    facingDirectionMap: {
        // facing_direction
        0: new Vector(0, -1, 0),
        1: new Vector(0, 1, 0),
        2: new Vector(0, 0, -1),
        3: new Vector(0, 0, 1),
        4: new Vector(-1, 0, 0),
        5: new Vector(1, 0, 0),
    },
    cardinalDirectionMap: {
        // minecraft:cardinal_direction
        north: new Vector(0, 0, -1),
        south: new Vector(0, 0, 1),
        west: new Vector(-1, 0, 0),
        east: new Vector(1, 0, 0),
    },
    pillarAxisMap: {
        // pillar_axis
        x_0: new Vector(1, 0, 0),
        y_0: new Vector(0, 1, 0),
        z_0: new Vector(0, 0, 1),
        x_1: new Vector(-1, 0, 0),
        y_1: new Vector(0, -1, 0),
        z_1: new Vector(0, 0, -1),
    },
    groundSignDirectionMap: {
        // ground_sign_direction
        0: new Vector(0, 0, 1),
        1: new Vector(0, 0, 1).rotate((1 / 16) * 360, "y"),
        2: new Vector(0, 0, 1).rotate((2 / 16) * 360, "y"),
        3: new Vector(0, 0, 1).rotate((3 / 16) * 360, "y"),
        4: new Vector(0, 0, 1).rotate((4 / 16) * 360, "y"),
        5: new Vector(0, 0, 1).rotate((5 / 16) * 360, "y"),
        6: new Vector(0, 0, 1).rotate((6 / 16) * 360, "y"),
        7: new Vector(0, 0, 1).rotate((7 / 16) * 360, "y"),
        8: new Vector(0, 0, 1).rotate((8 / 16) * 360, "y"),
        9: new Vector(0, 0, 1).rotate((9 / 16) * 360, "y"),
        10: new Vector(0, 0, 1).rotate((10 / 16) * 360, "y"),
        11: new Vector(0, 0, 1).rotate((11 / 16) * 360, "y"),
        12: new Vector(0, 0, 1).rotate((12 / 16) * 360, "y"),
        13: new Vector(0, 0, 1).rotate((13 / 16) * 360, "y"),
        14: new Vector(0, 0, 1).rotate((14 / 16) * 360, "y"),
        15: new Vector(0, 0, 1).rotate((15 / 16) * 360, "y"),
    },
    stairsMap: {
        // upside_down_bit - weirdo_direction
        false_0: new Vector(-1, 1, 0),
        false_1: new Vector(1, 1, 0),
        false_2: new Vector(0, 1, -1),
        false_3: new Vector(0, 1, 1),
        true_0: new Vector(-1, -1, 0),
        true_1: new Vector(1, -1, 0),
        true_2: new Vector(0, -1, -1),
        true_3: new Vector(0, -1, 1),
    },
    torchMap: {
        // torch_facing_direction
        north: new Vector(0, 0, 1),
        east: new Vector(-1, 0, 0),
        south: new Vector(0, 0, -1),
        west: new Vector(1, 0, 0),
        top: new Vector(0, 1, 0),
    },
    leverMap: {
        // lever_direction
        north: new Vector(0, 0, 1),
        east: new Vector(-1, 0, 0),
        south: new Vector(0, 0, -1),
        west: new Vector(1, 0, 0),
        up_north_south: new Vector(0, 1, 0.5),
        up_north_south0: new Vector(0, 1, -0.5),
        up_east_west: new Vector(0.5, 1, 0),
        up_east_west0: new Vector(-0.5, 1, 0),
        down_north_south: new Vector(0, -1, 0.5),
        down_north_south0: new Vector(0, -1, -0.5),
        down_east_west: new Vector(0.5, -1, 0),
        down_east_west0: new Vector(-0.5, -1, 0),
    },
    doorMap: {
        // door_hinge_bit - direction
        false_0: new Vector(1, 0, 0.5),
        false_1: new Vector(-0.5, 0, 1),
        false_2: new Vector(-1, 0, -0.5),
        false_3: new Vector(0.5, 0, -1),
        true_0: new Vector(1, 0, -0.5),
        true_1: new Vector(0.5, 0, 1),
        true_2: new Vector(-1, 0, 0.5),
        true_3: new Vector(-0.5, 0, -1),
    },
    bellMap: {
        // attachment - direction
        standing_0: new Vector(1, 0.5, 0),
        standing_1: new Vector(0, 0.5, 1),
        standing_2: new Vector(-1, 0.5, 0),
        standing_3: new Vector(0, 0.5, -1),
        side_0: new Vector(1, 0, 0),
        side_1: new Vector(0, 0, 1),
        side_2: new Vector(-1, 0, 0),
        side_3: new Vector(0, 0, -1),
        hanging_0: new Vector(1, -0.5, 0),
        hanging_1: new Vector(0, -0.5, 1),
        hanging_2: new Vector(-1, -0.5, 0),
        hanging_3: new Vector(0, -0.5, -1),
    },
    trapdoorMap: {
        // upside_down_bit - open_bit - direction
        false_false_0: new Vector(-0.5, 1, 0),
        false_false_1: new Vector(0.5, 1, 0),
        false_false_2: new Vector(0, 1, -0.5),
        false_false_3: new Vector(0, 1, 0.5),
        true_false_0: new Vector(-0.5, -1, 0),
        true_false_1: new Vector(0.5, -1, 0),
        true_false_2: new Vector(0, -1, -0.5),
        true_false_3: new Vector(0, -1, 0.5),
        false_true_0: new Vector(-1, 0.5, 0),
        false_true_1: new Vector(1, 0.5, 0),
        false_true_2: new Vector(0, 0.5, -1),
        false_true_3: new Vector(0, 0.5, 1),
        true_true_0: new Vector(-1, -0.5, 0),
        true_true_1: new Vector(1, -0.5, 0),
        true_true_2: new Vector(0, -0.5, -1),
        true_true_3: new Vector(0, -0.5, 1),
    },
    // TODO: Support glow lychen
};
