import { system, } from "@minecraft/server";
import CustomVector3 from "../calc/vector3";
import { SimplexNoise } from "../calc/noise";
function replaceBlocksInArea(dimension, center, range, replaceBlocks, maxReplacements = 100) {
    const replaceMap = new Map(replaceBlocks);
    let replacements = 0;
    const noise = new SimplexNoise();
    const batchSize = 100; // Number of blocks to update in a single batch
    const updateBatch = [];
    const minX = Math.floor(center.x - range);
    const minY = Math.floor(center.y - range);
    const minZ = Math.floor(center.z - range);
    const maxX = Math.ceil(center.x + range);
    const maxY = Math.ceil(center.y + range);
    const maxZ = Math.ceil(center.z + range);
    // Precompute the noise and distance
    const candidateBlocks = [];
    const threshold = 0.4; // Adjust as needed
    for (let x = minX; x <= maxX; x++) {
        for (let y = minY; y <= maxY; y++) {
            for (let z = minZ; z <= maxZ; z++) {
                if (replacements >= maxReplacements)
                    break;
                const distanceSquared = (x - center.x) ** 2 + (y - center.y) ** 2 + (z - center.z) ** 2;
                if (distanceSquared > range ** 2)
                    continue;
                const noiseValue = noise.noise3D(x * 0.1, y * 0.1, z * 0.1);
                if (noiseValue >= threshold) {
                    candidateBlocks.push({ x, y, z });
                }
            }
        }
    }
    // Use a Set for faster lookups
    const replaceBlockSet = new Set(replaceBlocks.map(([oldType]) => oldType));
    // Apply the block replacements
    for (const blockPos of candidateBlocks) {
        if (replacements >= maxReplacements)
            break;
        const block = dimension.getBlock(blockPos);
        if (block && replaceBlockSet.has(block.typeId)) {
            const newType = replaceMap.get(block.typeId);
            updateBatch.push({ pos: blockPos, newType });
            replacements++;
            if (updateBatch.length >= batchSize) {
                applyBatchUpdate(dimension, updateBatch);
                updateBatch.length = 0;
            }
        }
    }
    if (updateBatch.length > 0) {
        applyBatchUpdate(dimension, updateBatch);
    }
}
function applyBatchUpdate(dimension, batch) {
    // Batch update: apply changes in one go
    batch.forEach(({ pos, newType }) => {
        try {
            dimension.getBlock(pos)?.setType(newType);
        }
        catch (e) {
            console.error(`Failed to set block at ${pos}: ${e}`);
        }
    });
}
export function createLocationShockWave(dimension, location, options) {
    const { entity, centerParticles = [], radius = 10, speed = 0.5, startRange = 0, stepSize = 0.25, damage = 1, damageOptions, knockback = 2, knockbackRangeFunction = (kb) => [kb, 0.25], canRepeatEntityHit = true, entityHitFunction = () => true, expansionParticles = [], expansionParticlesOffset = 1, expansionParticlesStepSize = 4 * Math.PI, expansionParticlesMinRange = 1, expansionParticleChance = 1, // Default value
    verticalTolerance = 1, // New parameter with a default value
     } = options;
    spawnParticles(dimension, location, centerParticles);
    const hitEntities = new Set();
    for (let range = startRange; range < radius; range += stepSize) {
        system.runTimeout(() => {
            processShockWaveStep(dimension, location, range, stepSize, {
                entity,
                damage,
                damageOptions,
                knockback,
                knockbackRangeFunction,
                canRepeatEntityHit,
                entityHitFunction,
                hitEntities,
                verticalTolerance,
                replaceBlocks: options.replaceBlocks,
            });
            spawnExpansionParticles(dimension, location, range, {
                expansionParticles,
                expansionParticlesOffset,
                expansionParticlesStepSize,
                expansionParticlesMinRange,
                expansionParticleChance,
            });
        }, range / speed);
    }
}
function spawnParticles(dimension, location, particles) {
    particles.forEach((particle) => {
        try {
            dimension.spawnParticle(particle, location);
        }
        catch { }
    });
}
function processShockWaveStep(dimension, location, range, stepSize, options) {
    const { entity, damage, damageOptions, knockback, knockbackRangeFunction, canRepeatEntityHit, entityHitFunction, hitEntities, verticalTolerance = 1, // Default vertical tolerance of 1 block
    detectionRadius = 2, // Default detection radius
     } = options;
    const entitiesInRange = dimension.getEntities({
        location,
        minDistance: range,
        maxDistance: range + stepSize,
    });
    if (options.replaceBlocks && options.replaceBlocks.length > 0) {
        replaceBlocksInArea(dimension, location, range, options.replaceBlocks);
    }
    entitiesInRange.forEach((hitEntity) => {
        if (entity === hitEntity || (!canRepeatEntityHit && hitEntities.has(hitEntity))) {
            return;
        }
        // Check if the entity is within the vertical tolerance
        const verticalDistance = Math.abs(hitEntity.location.y - location.y);
        if (verticalDistance > verticalTolerance) {
            return;
        }
        if (!canRepeatEntityHit) {
            hitEntities.add(hitEntity);
        }
        const firstHit = !hitEntities.has(hitEntity);
        if (!canRepeatEntityHit || firstHit) {
            hitEntities.add(hitEntity);
        }
        entityHitFunction(hitEntity, firstHit);
        applyDamageAndKnockback(hitEntity, location, {
            damage,
            damageOptions,
            knockback,
            knockbackRangeFunction,
            range,
        });
    });
}
function applyDamageAndKnockback(entity, origin, options) {
    const { damage, damageOptions, knockback, knockbackRangeFunction, range } = options;
    if (damage > 0 && !entity.matches({families: ["tame"]})) {
        try {
            entity.applyDamage(damage, damageOptions);
        }
        catch { }
    }
    if (knockback > 0) {
        try {
            applyKnockback(entity, origin, knockback, knockbackRangeFunction, range);
        }
        catch { }
    }
}
function applyKnockback(entity, origin, knockback, knockbackRangeFunction, range) {
    const entityPos = CustomVector3.fromMC(entity.location);
    const originPos = CustomVector3.fromMC(origin);
    const direction = entityPos.subtract(originPos);
    // Project the direction onto the horizontal plane
    const horizontalDirection = new CustomVector3(direction.x, 0, direction.z).normalize();
    const [horizontalKnockback, verticalKnockback] = knockbackRangeFunction(knockback, range);
    // Calculate vertical component separately
    const verticalComponent = direction.y > 0 ? verticalKnockback : -verticalKnockback;
    const knockbackScale = 0.5;
    try {
        entity.applyKnockback(horizontalDirection.x, horizontalDirection.z, horizontalKnockback * knockbackScale, verticalComponent * knockbackScale);
    }
    catch {
        // Some entities may not support knockback
    }
}
function spawnExpansionParticles(dimension, location, range, options) {
    const { expansionParticles, expansionParticlesOffset, expansionParticlesStepSize, expansionParticlesMinRange, expansionParticleChance, } = options;
    if (expansionParticles.length === 0 || range < expansionParticlesMinRange) {
        return;
    }
    const expansionParticlesCount = Math.ceil((2 * Math.PI * range) / expansionParticlesStepSize);
    const theta = (2 * Math.PI) / expansionParticlesCount;
    const offset = Math.random() * theta;
    for (let i = 0; i < expansionParticlesCount; i++) {
        if (Math.random() > expansionParticleChance)
            continue; // Add chance check
        const x = range * Math.cos(i * theta + offset);
        const z = range * Math.sin(i * theta + offset);
        const particleLocation = new CustomVector3(location.x, location.y, location.z).add(new CustomVector3(x, expansionParticlesOffset, z));
        spawnParticles(dimension, particleLocation, expansionParticles);
    }
}
export function createEntityShockWave(entity, options) {
    createLocationShockWave(entity.dimension, entity.location, { ...options, entity });
}
export function createForwardShockWave(entity, options) {
    const { direction, width, length, height, centerParticles = [], speed = 0.5, startRange = 0, stepSize = 0.25, // This will control the shockwave's behavior
    damage = 1, damageOptions, knockback = 2, knockbackRangeFunction = (kb) => [kb, 0.25], canRepeatEntityHit = true, entityHitFunction = () => true, expansionParticles = [], expansionParticlesOffset = 1, expansionParticlesStepSize = 1, // This will control particle spacing
    expansionParticlesMinRange = 1, expansionParticleChance = 1, expansionParticlesWidthCount = width, firstOffset = 1, // Default offset of 1 block
     } = options;
    const dimension = entity.dimension;
    const origin = CustomVector3.fromMC(entity.location); // Convert to custom vector for calculations
    const forwardVector = CustomVector3.fromMC(direction).normalize();
    const rightVector = forwardVector.cross(new CustomVector3(0, 1, 0)).normalize();
    // Apply the firstOffset to the origin
    const offsetOrigin = origin.add(forwardVector.scale(firstOffset));
    // Spawn initial center particles (if any) but offset from the entity
    spawnParticles(dimension, offsetOrigin, centerParticles);
    const hitEntities = new Set();
    // Handle shockwave progression with `stepSize` (damage/hit logic)
    for (let range = startRange; range < length; range += stepSize) {
        system.runTimeout(() => {
            processForwardShockWaveStep(dimension, offsetOrigin, // Use the offset origin for calculations
            range, stepSize, width, height, forwardVector, rightVector, {
                entity,
                damage,
                damageOptions,
                knockback,
                knockbackRangeFunction,
                canRepeatEntityHit,
                entityHitFunction,
                hitEntities,
                replaceBlocks: options.replaceBlocks,
            });
        }, range / speed);
    }
    // Handle particle spawning with `expansionParticlesStepSize` starting from firstOffset
    for (let particleRange = startRange; particleRange < length; particleRange += expansionParticlesStepSize) {
        system.runTimeout(() => {
            spawnForwardExpansionParticles(dimension, offsetOrigin, // Use the offset origin for particle spawning
            particleRange, width, height, forwardVector, rightVector, {
                expansionParticles,
                expansionParticlesOffset,
                expansionParticlesStepSize,
                expansionParticlesMinRange,
                expansionParticleChance,
                expansionParticlesWidthCount,
            });
        }, particleRange / speed);
    }
}
function processForwardShockWaveStep(dimension, origin, range, stepSize, width, height, forwardVector, rightVector, options) {
    const detectionRadius = options.detectionRadius || 2; // Default detection radius
    const halfWidth = width / 2;
    const halfHeight = height / 2;
    const startPoint = CustomVector3.fromMC(origin).add(forwardVector.scale(range - stepSize / 2));
    const endPoint = startPoint.add(forwardVector.scale(stepSize));
    // Calculate the up vector perpendicular to both forwardVector and rightVector
    const upVector = forwardVector.cross(rightVector).normalize();
    if (options.replaceBlocks && options.replaceBlocks.length > 0) {
        const center = CustomVector3.fromMC(origin).add(forwardVector.scale(range));
        const replacementRange = Math.max(width, height) / 2;
        replaceBlocksInArea(dimension, center, replacementRange, options.replaceBlocks);
    }
    for (let w = -halfWidth; w <= halfWidth; w += stepSize) {
        for (let h = -halfHeight; h <= halfHeight; h += stepSize) {
            const offset = rightVector.scale(w).add(upVector.scale(h));
            const lineStart = startPoint.add(offset);
            const lineEnd = endPoint.add(offset);
            const entitiesInLine = dimension.getEntities({
                location: lineStart,
                maxDistance: detectionRadius,
            });
            entitiesInLine.forEach((hitEntity) => {
                if (options.entity === hitEntity ||
                    (!options.canRepeatEntityHit && options.hitEntities.has(hitEntity))) {
                    return;
                }
                const distanceToEntity = CustomVector3.fromMC(hitEntity.location).distanceTo(lineStart);
                if (distanceToEntity > detectionRadius) {
                    return;
                }
                if (!options.canRepeatEntityHit) {
                    options.hitEntities.add(hitEntity);
                }
                const firstHit = !options.hitEntities.has(hitEntity);
                if (!options.canRepeatEntityHit || firstHit) {
                    options.hitEntities.add(hitEntity);
                }
                options.entityHitFunction(hitEntity, firstHit);
                applyDamageAndKnockback(hitEntity, origin, {
                    damage: options.damage,
                    damageOptions: options.damageOptions,
                    knockback: options.knockback,
                    knockbackRangeFunction: options.knockbackRangeFunction,
                    range,
                });
            });
        }
    }
}
function spawnForwardExpansionParticles(dimension, origin, range, width, height, forwardVector, rightVector, options) {
    const { expansionParticles, expansionParticlesOffset, expansionParticlesStepSize, expansionParticlesMinRange, expansionParticleChance, expansionParticlesWidthCount, } = options;
    // Ensure particles are only spawned if range is beyond the minimum range and particle count > 0
    if (expansionParticles.length === 0 || range < expansionParticlesMinRange) {
        return;
    }
    const halfWidth = width / 2;
    const halfHeight = height / 2;
    // Ensure at least 1 particle across the width if the width count is very small
    const particlesWidthCount = Math.max(expansionParticlesWidthCount, 1);
    // Step size along the width, based on the desired number of particle columns
    const stepWidth = width / Math.max(particlesWidthCount - 1, 1);
    // Adjust the distance between particles in the forward direction based on expansionParticlesStepSize
    const forwardStep = expansionParticlesStepSize;
    // Calculate the up vector perpendicular to both forwardVector and rightVector
    const upVector = forwardVector.cross(rightVector).normalize();
    // Iterate over distance forward, independent of shockwave stepSize
    for (let forwardDist = 0; forwardDist <= range; forwardDist += forwardStep) {
        // Ensure particle chance
        if (Math.random() > expansionParticleChance)
            continue;
        // For each step forward, spawn particles based on width count
        for (let i = 0; i < particlesWidthCount; i++) {
            const w = particlesWidthCount === 1 ? 0 : -halfWidth + i * stepWidth; // Spawn only 1 particle if width count = 1
            const particlePos = CustomVector3.fromMC(origin)
                .add(forwardVector.scale(forwardDist)) // Position along the forward direction
                .add(rightVector.scale(w)) // Position along the width
                .add(upVector.scale(expansionParticlesOffset)); // Vertical offset for particles
            // Avoid spawning multiple particles at very close positions due to rounding issues
            if (isFinite(particlePos.x) && isFinite(particlePos.y) && isFinite(particlePos.z)) {
                spawnParticles(dimension, particlePos, expansionParticles);
            }
        }
    }
}
export function createEntityForwardShockWave(entity, options) {
    const direction = entity.getViewDirection();
    createForwardShockWave(entity, { ...options, direction });
}
