import { BlockVolume, ListBlockVolume } from "@minecraft/server";
import { assertCanBuildWithin } from "./../modules/assert.js";
import { Mask } from "./../modules/mask.js";
import { regionIterateBlocks, regionIterateChunks, regionVolume, Vector } from "./../../library/Minecraft.js";
import { getWorldHeightLimits, snap } from "../util.js";
import { Jobs } from "./../modules/jobs.js";
var ChunkStatus;
(function (ChunkStatus) {
    /** Empty part of the shape. */
    ChunkStatus[ChunkStatus["EMPTY"] = 0] = "EMPTY";
    /** Completely filled part of the shape. */
    ChunkStatus[ChunkStatus["FULL"] = 1] = "FULL";
    /** Partially filled part of the shape. */
    ChunkStatus[ChunkStatus["DETAIL"] = 2] = "DETAIL";
})(ChunkStatus || (ChunkStatus = {}));
// const shapeCache: Record<string, BlockVolumeBase[]> = {};
/**
 * A base shape class for generating blocks in a variety of formations.
 */
export class Shape {
    constructor() {
        /**
         * Whether the shape is being used in a brush.
         * Shapes used in a brush may handle history recording differently from other cases.
         */
        this.usedInBrush = false;
    }
    /**
     * Deduces what kind of chunk is being processed.
     * @param relLocMin relative location of the chunks minimum block corner
     * @param relLocMax relative location of the chunks maximum block corner
     * @param genVars
     * @returns ChunkStatus
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    getChunkStatus(relLocMin, relLocMax, genVars) {
        return ChunkStatus.DETAIL;
    }
    /**
     * Returns blocks that are in the shape.
     */
    *getBlocks(loc, options) {
        const range = this.getRegion(loc);
        this.genVars = {};
        this.prepGeneration(this.genVars, options);
        for (const block of regionIterateBlocks(...range)) {
            if (this.inShape(Vector.sub(block, loc).floor(), this.genVars))
                yield block;
        }
    }
    inShapeHollow(relLoc, genVars) {
        const block = this.inShape(relLoc, genVars);
        if (genVars.hollow && block) {
            const thickness = genVars.hollowThickness ?? 1;
            let neighbourCount = 0;
            for (const offset of [
                [0, thickness, 0],
                [0, -thickness, 0],
                [thickness, 0, 0],
                [-thickness, 0, 0],
                [0, 0, thickness],
                [0, 0, -thickness],
            ]) {
                neighbourCount += this.inShape(relLoc.add(offset), genVars) ? 1 : 0;
            }
            return neighbourCount == 6 ? false : block;
        }
        else {
            return block;
        }
    }
    drawShape(vertices, edges) {
        const edgePoints = [];
        for (const edge of edges) {
            const [a, b] = [vertices[edge[0]], vertices[edge[1]]];
            const resolution = Math.min(Math.floor(b.sub(a).length), 16);
            for (let i = 1; i < resolution; i++) {
                const t = i / resolution;
                edgePoints.push(a.lerp(b, t));
            }
        }
        return vertices.concat(edgePoints).map((v) => ["wedit:selection_draw", v]);
    }
    drawCircle(center, radius, axis) {
        const vec = axis === "x" ? new Vector(0, 1, 0) : axis === "y" ? new Vector(1, 0, 0) : new Vector(0, 1, 0);
        const resolution = snap(Math.min(radius * 2 * Math.PI, 36), 4);
        const points = [];
        for (let i = 0; i < resolution; i++) {
            let point = vec.rotate((i / resolution) * 360, axis);
            point = point.mul(radius).add(center).add(0.5);
            points.push(["wedit:selection_draw", point]);
        }
        return points;
    }
    /**
     * Generates a block formation at a certain location.
     * @param loc The location the shape will be generated at
     * @param pattern The pattern that the shape will be made with
     * @param mask The mask to decide where the shape will generate blocks
     * @param session The session that's using this shape
     * @param options A group of options that can change how the shape is generated
     */
    *generate(loc, pattern, mask, session, options) {
        const [min, max] = this.getRegion(loc);
        const player = session.getPlayer();
        const dimension = player.dimension;
        const [minY, maxY] = getWorldHeightLimits(dimension);
        min.y = Math.max(minY, min.y);
        max.y = Math.min(maxY, max.y);
        const canGenerate = max.y >= min.y;
        pattern = pattern.withContext(session, [min, max]);
        const simplePattern = pattern.isSimple();
        if (!Jobs.inContext())
            assertCanBuildWithin(player, min, max);
        let blocksAffected = 0;
        const volumes = [];
        const history = options?.recordHistory ?? true ? session.getHistory() : undefined;
        const record = history?.record(this.usedInBrush);
        if (!canGenerate) {
            history?.commit(record);
            yield Jobs.nextStep("Calculating shape...");
            yield Jobs.nextStep("Generating blocks...");
            return 0;
        }
        try {
            let count = 0;
            this.genVars = {};
            this.prepGeneration(this.genVars, options);
            // TODO: Localize
            let activeMask = mask ?? new Mask();
            const globalMask = options?.ignoreGlobalMask ?? false ? new Mask() : session.globalMask;
            activeMask = (!activeMask ? globalMask : globalMask ? activeMask.intersect(globalMask) : activeMask)?.withContext(session);
            const simpleMask = activeMask.isSimple();
            let progress = 0;
            const volume = regionVolume(min, max);
            const inShapeFunc = this.customHollow ? "inShape" : "inShapeHollow";
            yield Jobs.nextStep("Calculating shape...");
            // Collect blocks and areas that will be changed.
            for (const [chunkMin, chunkMax] of regionIterateChunks(min, max)) {
                yield Jobs.setProgress(progress / volume);
                const chunkStatus = this.getChunkStatus(Vector.sub(chunkMin, loc).floor(), Vector.sub(chunkMax, loc).floor(), this.genVars);
                if (chunkStatus === ChunkStatus.FULL && simpleMask) {
                    const volume = regionVolume(chunkMin, chunkMax);
                    progress += volume;
                    blocksAffected += volume;
                    volumes.push(new BlockVolume(chunkMin, chunkMax));
                }
                else if (chunkStatus === ChunkStatus.EMPTY) {
                    const volume = regionVolume(chunkMin, chunkMax);
                    progress += volume;
                }
                else {
                    const blocks = [];
                    for (const blockLoc of regionIterateBlocks(chunkMin, chunkMax)) {
                        yield Jobs.setProgress(progress / volume);
                        progress++;
                        if (this[inShapeFunc](Vector.sub(blockLoc, loc).floor(), this.genVars)) {
                            const block = dimension.getBlock(blockLoc) ?? (yield* Jobs.loadBlock(blockLoc));
                            if (simpleMask || activeMask.matchesBlock(block)) {
                                blocks.push(block);
                                blocksAffected++;
                            }
                        }
                        yield;
                    }
                    if (blocks.length)
                        volumes.push(simplePattern ? new ListBlockVolume(blocks) : blocks);
                }
            }
            progress = 0;
            yield Jobs.nextStep("Generating blocks...");
            if (history)
                yield* history.addUndoStructure(record, min, max);
            const maskInSimpleFill = simpleMask ? activeMask : undefined;
            for (const volume of volumes) {
                yield Jobs.setProgress(progress / blocksAffected);
                if (Array.isArray(volume)) {
                    for (let block of volume) {
                        if (!block.isValid && Jobs.inContext())
                            block = yield* Jobs.loadBlock(loc);
                        if ((!maskInSimpleFill || maskInSimpleFill.matchesBlock(block)) && pattern.setBlock(block))
                            count++;
                        progress++;
                    }
                }
                else {
                    if (Jobs.inContext())
                        yield* Jobs.loadArea(volume.getMin(), volume.getMax());
                    count += pattern.fillBlocks(dimension, volume, maskInSimpleFill);
                    progress += volume.getCapacity();
                }
            }
            if (history)
                yield* history.addRedoStructure(record, min, max);
            history?.commit(record);
            return count;
        }
        catch (e) {
            history?.cancel(record);
            throw e;
        }
    }
}
Shape.ChunkStatus = ChunkStatus;
