import { world, system, ItemStack, EntityComponentTypes, Player, GameMode } from "@minecraft/server";
import * as dogData from "./dogData.js";
const DEBUG = false;

export function getToyItem(dog) {
    const toyType = dog.getProperty(dogData.HELD_TOY_PROPERTY);
    if (!toyType) return null;

    const toyConfig = Object.values(dogData.dogToyMappings).find(
        (toy) => toy.properyName === toyType
    );

    return toyConfig ? new ItemStack(toyConfig.itemTypeId, 1) : null;
}

export function getToyData(throwableItemEntity) {
    if (!throwableItemEntity?.typeId) return null;

    const toyConfig = Object.values(dogData.dogToyMappings).find(
        (toy) => toy.enityTypeId === throwableItemEntity.typeId
    );

    return toyConfig;
}

function dropToy(dog) {
    // Get the held toy
    const toyItem = getToyItem(dog);
    if (!toyItem) return;

    let gaveToy = false;

    // Try to give the toy to the owner
    const dogOwner = dog.getComponent(EntityComponentTypes.Tameable)?.tamedToPlayerId ?? undefined;
    const owner = world.getEntity(dogOwner);
    if (owner) {
        const dogPos = dog.getHeadLocation();
        const dx = dogPos.x - owner.location.x;
        const dy = dogPos.y - owner.location.y;
        const dz = dogPos.z - owner.location.z;
        const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);

        if (distance <= 2.5) {
            const inventory = owner.getComponent("minecraft:inventory")?.container;
            if (inventory && inventory?.emptySlotsCount > 0) {
                inventory.addItem(toyItem);
                gaveToy = true;
            }
        }
    }

    // Drop the toy
    if (!gaveToy) try { dog.dimension.spawnItem(toyItem, dog.getHeadLocation()); } catch (e) { }

    // Reset Dog (handled by entity event)
    // dog.setProperty('oct_dogs:held_toy', 'none');
    // dog.triggerEvent('oct_dogs:calm');
}

// Dog caught a toy
world.afterEvents.entityHitEntity.subscribe((e) => {
    const { damagingEntity, hitEntity } = e;

    if (!hitEntity.isValid || !damagingEntity.isValid) return;

    // Dog caught a toy
    if (hitEntity.getComponent(EntityComponentTypes.TypeFamily)?.hasTypeFamily("oct_dogs:throwable_toy")) {
        const toyData = getToyData(hitEntity);
        if (!toyData || !toyData.properyName) return;

        damagingEntity.setProperty(dogData.HELD_TOY_PROPERTY, toyData.properyName);
        damagingEntity.triggerEvent('oct_dogs:has_toy');

        if (toyData.catchSound) hitEntity.dimension.playSound(toyData.catchSound, hitEntity.location);

        hitEntity.remove();

    }

    // Dog returns a toy
    else if (hitEntity.getComponent(EntityComponentTypes.TypeFamily)?.hasTypeFamily("oct_dogs:fetching_dummy")) {
        // Drop the toy
        dropToy(damagingEntity);

        // Find and reset the player that threw the toy
        const playerId = hitEntity.getDynamicProperty("oct_dogs:player_id");
        const player = world.getEntity(playerId);
        if (player && player?.fetchingManager) {
            player.fetchingManager.stopCreativeFetching();
            return;
        }

        // Remove the dummy
        hitEntity?.remove();
    }

}, { entityTypes: [...dogData.dogsThatPlayWithToys] });

// Dog returned the toy
system.afterEvents.scriptEventReceive.subscribe((e) => {
    const { id, message, sourceEntity } = e;
    if (id == "oct_dogs:dog" && message == "return_toy") {
        dropToy(sourceEntity);

        // Find and reset the player that threw the toy
        for (const e of sourceEntity.dimension.getEntities(
            {
                location: sourceEntity.location,
                families: ["player"],
                maxDistance: 3
            }
        )) {
            if (e?.fetchingManager) {
                e.fetchingManager.stopCreativeFetching();
            }
        }
    };

}, { namespaces: ["oct_dogs"] });

// Drop toy on hurt
world.afterEvents.entityHurt.subscribe((e) => {
    const { hurtEntity, damagingEntity } = e;
    if (!hurtEntity.isValid) return;
    if (hurtEntity?.getProperty(dogData.HELD_TOY_PROPERTY) != dogData.dogToyMappings.NONE.properyName) {
        dropToy(hurtEntity);
        hurtEntity?.triggerEvent("oct_dogs:stop_chasing_toy");

        // Find and reset the player that threw the toy
        if (damagingEntity && damagingEntity?.fetchingManager) {
            damagingEntity.fetchingManager.stopCreativeFetching();
        }
    };

}, { entityTypes: [...dogData.dogsThatPlayWithToys] });


export function startTrackingBouncyToy(toy, params) {
    const creationTime = system.currentTick;
    let lastBounceTick = -100;

    const handle = system.runInterval(() => {
        try {
            if (!toy?.isValid) { system.clearRun(handle); return; }

            const result = bouncyToyTick(toy, params, { lastBounceTick, setLast(t) { lastBounceTick = t; } });
            if (result === "stopped" && system.currentTick > creationTime + 1) {
                // console.log("§ctoy stopped bouncing");

                system.clearRun(handle);
                toy.setProperty("oct_dogs:is_flying", false);
            }
        } catch (e) {
            if (DEBUG) console.log(`§cError: ${e}`);

            system.clearRun(handle);
            if (toy?.isValid) toy?.setProperty("oct_dogs:is_flying", false);
        }
    });
}
function bouncyToyTick(toy, params, state) {
    const v = toy.getVelocity();
    const speed = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
    const touchingGround = getBlockCollisions(toy, params, [{ x: 0, y: -1, z: 0 }]).length > 0;

    // Only stop if it's slow AND on the ground
    if (speed < 0.1 && touchingGround) {
        return "stopped";
    }

    const hits = getBlockCollisions(toy, params);
    if (hits.length === 0) return "continue";

    // Avoid multiple reflections in the same tick
    if (system.currentTick === state.lastBounceTick) return "continue";
    state.setLast(system.currentTick);

    const collisionSound = params.collisionSound ?? "oct_dogs.dog_toy.squeaky.impact"
    const restitution = params.restitution ?? 0.6;      // how bouncy (0..1)
    const groundBoost = params.groundBoost ?? 0.3;

    const travelBoost = 1.4
    const minBounceSpeed = 0.15; // below this after reflection, we stop
    const nudgeStrength = 0.05; // small velocity nudge

    let newV = { ...v };

    for (const dir of hits) {
        // Surface normal is opposite of the cast direction
        const n = { x: -dir.x, y: -dir.y, z: -dir.z };
        const vdotn = newV.x * n.x + newV.y * n.y + newV.z * n.z;
        const horizontalSpeed = Math.sqrt(newV.x ** 2 + newV.z ** 2);

        // Reflect
        newV = {
            x: (newV.x - 2 * vdotn * n.x) * restitution * travelBoost,
            y: ((newV.y - 2 * vdotn * n.y) * restitution) + (dir.is_down ? horizontalSpeed * groundBoost : 0),
            z: (newV.z - 2 * vdotn * n.z) * restitution * travelBoost
        };

        // Add a small velocity nudge along normal to prevent sticking
        newV.x += n.x * nudgeStrength;
        newV.z += n.z * nudgeStrength;
        // newV.y += n.y * nudgeStrength; // no y

        // enable for one contact per tick
        break;
    }

    // If speed after reflection is too small, stop it
    const newSpeed = Math.sqrt(newV.x ** 2 + newV.y ** 2 + newV.z ** 2);
    if ((newSpeed < minBounceSpeed) && touchingGround) return "stopped";

    toy.clearVelocity();
    toy.applyImpulse(newV);
    if (newSpeed > 0.2 && collisionSound) toy.dimension.playSound(collisionSound, toy.location, { volume: 2 });

    return "continue";
}
const collisionTypes = {
    ball: [
        { x: 1, y: 0, z: 0 }, // east
        { x: -1, y: 0, z: 0 }, // west
        { x: 0, y: 1, z: 0 }, // up
        { x: 0, y: -1, z: 0, is_down: true }, // down
        { x: 0, y: 0, z: 1 }, // south
        { x: 0, y: 0, z: -1 }, // north
    ],
    disc: [
        { x: 1, y: 0, z: 0 }, // east
        { x: -1, y: 0, z: 0 }, // west
        { x: 0, y: 0, z: 1 }, // south
        { x: 0, y: 0, z: -1 }, // north
    ]
}
function getBlockCollisions(toy, params, dirsOverride) {
    const hits = [];
    const pos = toy.location;
    const collisionType = params.collisionType ?? "ball"
    const offset = params.collisionSize ?? 0.2; // how far from center to probe

    const dirs = dirsOverride ?? collisionTypes[collisionType];

    const centerY = pos.y + offset / 2;

    for (const d of dirs) {
        const probe = {
            x: pos.x + d.x * offset,
            y: centerY + d.y * offset,
            z: pos.z + d.z * offset,
        };

        const block = toy.dimension.getBlock(probe);
        if (block && !block.isAir && !block.isLiquid) {
            hits.push(d); // collision normal
            // world.sendMessage(`Collision: ${JSON.stringify(d)}, type: ${collisionType}`);
        }
    }

    return hits;
}


class FetchingManager {
    constructor(player) {
        this.player = player;
        this.fetchingEntity = null;
        this.handle = null;
        this.startTime = null;
    }
    startCreativeFetching() {
        if (DEBUG) world.sendMessage(`§aFetching Start: ${this.player.name}`);

        if (this.handle || this.fetchingEntity) this.stopCreativeFetching(); // Reset if already fetching

        try {
            this.fetchingEntity = this.player.dimension.spawnEntity("oct_dogs:fetching_dummy", this.player.getHeadLocation());
        } catch (e) {
            if (DEBUG) console.log(`§cSpawn error: ${e}`);
            return;
        }
        if (!this.fetchingEntity) return;

        this.fetchingEntity.setDynamicProperty("oct_dogs:player_id", this.player.id);
        this.startTime = system.currentTick;

        this.handle = system.runInterval(() => {
            if (!this.fetchingEntity?.isValid) { // Reset if not a valid entity
                this.stopCreativeFetching();
                return;
            }

            const gameMode = this.player.getGameMode();
            if (gameMode != GameMode.Creative) { // Reset if no longer in creative
                this.stopCreativeFetching();
                return;
            }

            if (system.currentTick > this.startTime + 400) { // Reset after some time
                this.stopCreativeFetching();
                return;
            }

            try {
                this.fetchingEntity.teleport(this.player.getHeadLocation());

            } catch (e) {
                if (DEBUG) console.log(`§cError: ${e}`);
                this.stopCreativeFetching(); // Reset
            }
        });
    }
    stopCreativeFetching() {
        if (DEBUG) world.sendMessage(`§cFetching End: ${this.player.name}`);

        if (this.handle) {
            system.clearRun(this.handle);
            this.handle = null;
        }
        if (this.fetchingEntity?.isValid) {
            this.fetchingEntity.remove();
        }
        this.fetchingEntity = null;
    }
}

Object.assign(Player.prototype, {
    fetchingManager: null,
    initializeFetchingManager() {
        this.fetchingManager = new FetchingManager(this);
    }
});


// world.afterEvents.playerJoin.subscribe((e) => { // Initialize fetching
//     const interval = system.runInterval(() => {
//         const player = world.getAllPlayers().find((val) => val.id === e.playerId);
//         if (player?.isValid && !player.fetchingManager) {
//             player.initializeFetchingManager();
//             system.clearRun(interval);
//         }
//     });
// });

// NOTE: Has to run on interval in case the server restarts
system.runInterval(() => { // Initialize fetching
    for (const player of world.getPlayers()) {
        if (!player.fetchingManager) player.initializeFetchingManager();
    }
}, 10);

world.beforeEvents.playerLeave.subscribe((e) => { // Reset if player leaves
    if (e.player.fetchingManager) e.player.fetchingManager?.stopCreativeFetching();
})
world.afterEvents.entityDie.subscribe((e) => { // Reset if player dies
    if (!(e.deadEntity instanceof Player)) return;
    if (e.deadEntity.fetchingManager) e.deadEntity.fetchingManager?.stopCreativeFetching();
}, { entityTypes: ["minecraft:player"] });