import { EntityComponentTypes, EntityDamageCause, system } from "@minecraft/server";
import { couplingData, getCarSize, getPullingCar, getSpeed, locomotiveFamily, railPathDirection, trainFamily } from "./trainsAddOn";
import { createVector, distance, sendDebugMessage, vectorAbove, vectorMultiplyByNumber, vectorRoundDown, vectorToString } from "./JM_Utils";
import { speedLimitInterval } from "./automatic";

const lengthScalingFactor = 0.6

// is the given car a locomotive?
function isLocomotive(car) {
    const familyComponent = car.getComponent(EntityComponentTypes.TypeFamily)
    if (!familyComponent) return false
    if (familyComponent.hasTypeFamily(locomotiveFamily)) return true
    return false
}

// find the locomotive leading this train.  If there is none, returns undefined.  If the given car IS the locomotive, returns the given car.
function getTrainLocomotive(car) {
    while (!isLocomotive(car)) {
        car = getPullingCar(car, car.dimension)
        if (!car) return undefined
    }
    return car
}

// calculates given car's collision box and finds all entities whose collision is inside this box,
// then collides with them
export function checkCollision(car) {
    const collisionData = collisionDelay(car)
    // only if entity is old enough (prevent spawn collision)
    if (!collisionData.canCollide()) return
    // record last collision
    // collisionData.recordCollision()

    const velocity = car.getVelocity()
    const speed = getSpeed(velocity)
    const locomotive = getTrainLocomotive(car)
    // no collisions possible unless part of a train (has locomotive)
    if (!locomotive) return
    // only collide if train is set to at least speed 2
    if (locomotive.getProperty("lifeboat_ta:speed") < 1)  return

    const carLength = getCarSize(car.typeId) * lengthScalingFactor
    const carWidth = 0.5
    const angle = car.getRotation().y

    const nearbyEntities = car.dimension.getEntities({location: car.location, maxDistance: carLength + 3})
    const riders = []
    car.getComponent(EntityComponentTypes.Rideable)?.getRiders().forEach(rider => riders.push(rider.id))
    for (let i = nearbyEntities.length - 1; i >= 0; i--) {
        const entity = nearbyEntities[i]
        // ignore us
        if (entity.id === car.id) {
            nearbyEntities.splice(i,1)
            continue
        }
        // if this entity is riding this car, ignore
        if (riders.includes(entity.id)) {
            nearbyEntities.splice(i,1)
            continue
        }

        // ignore the car pulled by us
        if (couplingData(car).getId() === entity.id) continue

        // if entity is below or above train
        if (entity.getHeadLocation().y < Math.floor(car.location.y) || entity.location.y > Math.floor(car.location.y) + 2) continue

        const usCollision = new hitbox(carWidth, carLength, car.location, angle)

        if (entity.hasComponent(EntityComponentTypes.TypeFamily) && entity.getComponent(EntityComponentTypes.TypeFamily).hasTypeFamily(trainFamily)) {
            // it's a train; consider the entity a box

            // ignore the car pulling us
            if (couplingData(entity).getId() === car.id) continue

            const themCollision = new hitbox(carWidth, getCarSize(entity.typeId) * lengthScalingFactor, entity.location, entity.getRotation().y)
            if (usCollision.collidesWith(themCollision)) {
                if (speed >= (speedLimitInterval * 2)) {
                    const collisionVector = getVectorFromAToB(car.location, entity.location)
                    impulseBack(entity, collisionVector, speed)

                    let breakRail = false
                    let particle = "minecraft:large_explosion"
                    let particles = Math.floor(speed / speedLimitInterval)
                    let sound
                    if (car.getComponent(EntityComponentTypes.TypeFamily).hasTypeFamily(locomotiveFamily)) {
                        // only locomotives mess up the track
                        breakRail = true
                        sound = "ta_c"
                        particle = "minecraft:huge_explosion_emitter"
                        const reverse = vectorMultiplyByNumber(collisionVector, -1)
                        reverse.y = 0
                        try {
                            car.applyImpulse(reverse)
                        } catch (error) {
                            sendDebugMessage(`error - reverse: ${JSON.stringify(reverse)}, collision: ${JSON.stringify(collisionVector)}`)
                        }
                        
                        const usVect = vectorMultiplyByNumber(getRandomPerpendicularUnit(collisionVector),10)
                        usVect.y = 0.0
                        car.applyImpulse(usVect)
                        const themVect = vectorMultiplyByNumber(getRandomPerpendicularUnit(reverse),10)
                        themVect.y = 0.0
                        entity.applyImpulse(themVect)
                    }

                    if (sound) {
                        entity.dimension.playSound(sound, entity.location)
                        entity.dimension.getPlayers({location: entity.location, maxDistance: 500, minDistance: 16}).forEach(player => {
                            system.runTimeout(() => player.playSound("ta_d_c"),Math.floor(distance(player.location, entity.location) / 5))
                        })
                    }

                    // break rails, spawn particles
                    try {
                        var entityLocation = entity.dimension.getBlock(entity.location)
                        var carLocation = car.dimension.getBlock(car.location)
                        const path = railPathDirection(entityLocation, carLocation, true)?.path
                        if (path) {
                            path.forEach(railLocation => {
                                if (breakRail) entity.dimension.runCommand(`setblock ${vectorToString(railLocation)} air destroy`)
                                for (let i = 0; i < particles; i++) entity.dimension.spawnParticle(particle, vectorAbove(railLocation,1))
                            })
                        } else {
                            entity.dimension.spawnParticle(particle, vectorAbove(car.location,1))
                            entity.dimension.spawnParticle(particle, vectorAbove(entity.location,1))
                        }
                    } catch (error) {
                        // ignore
                        entity.dimension.runCommand(`setblock ${vectorToString(vectorRoundDown(car.location))} air destroy`)
                        entity.dimension.runCommand(`setblock ${vectorToString(vectorRoundDown(entity.location))} air destroy`)
                    }
                } else {
                    // slow speed collision, ignore?
                }
            }
        } else {
            // it's not a train; consider the entity as a point
            if (speed >= (speedLimitInterval * 2) && usCollision.containsPoint(entity.location, velocity)) {
                // collided!
                const collisionVector = getVectorFromAToB(car.location, entity.location)
                impulseBack(entity, collisionVector, speed)
                if (entity.hasComponent(EntityComponentTypes.Health)) {
                    // not a train, but has health; damage entity!
                    const baseDamage = Math.pow((speed / speedLimitInterval).toFixed(2), 1.5)
                    const damage = baseDamage + ((Math.random() * 4) - 2)
                    sendDebugMessage(`applying ${damage} damage to ${entity.typeId} ${entity.name}`)
                    entity.applyDamage(damage,{cause: EntityDamageCause.contact, damagingEntity: car})
                }
            }
        }
    }
}

// this applies the "equal and opposite" force to cars in a collision
function impulseBack(entity, collisionVector, speed) {
    try {
        entity.applyImpulse({
            x: collisionVector.x * speed * 0.3,
            y: speed * 0.2,
            z: collisionVector.z * speed * 0.3
        })
    } catch (error) {
        // cannot use applyImpulse on players, try knockback
        try {
            entity.applyKnockback(collisionVector.x, collisionVector.z, speed * 4, speed * 1.3)
        } catch (error) {
            // can't use impulse, can't use knockback
            // at this point we don't care anymore, ignore
        }
    }
}

function getRandomPerpendicularUnit(vect) {
    return normalize(getRandomPerpendicular(vect))
}

function normalize(vect) {
    const length = Math.sqrt(vect.x * vect.x + vect.z * vect.z);
    return length === 0 ? { x: 0, z: 0 } : { x: vect.x / length, z: vect.z / length };
}

function getRandomPerpendicular(vect) {
    return Math.random() < 0.5 ? { x: -vect.z, z: vect.x } : { x: vect.z, z: -vect.x }
}

class hitbox {
    constructor(halfWidth, halfLength, center, angle) {
        this.halfWidth = halfWidth
        this.halfLength = halfLength
        this.center = center
        this.angle = angle
    }

    /** determines if the point is within this hitbox.
     * if velocity is provided returns whether the point is in the front half of the hitbox (front relative to velocity)
     */
    containsPoint(point, usVelocity) {
        // Convert angle to radians and adjust from your system to math system
        // const angleRad = (Math.PI / 180) * (90 - angleDegrees);
        const angleRad = (-this.angle) * (Math.PI / 180);


        // Translate point to rectangle's center
        const dx = point.x - this.center.x;
        const dz = point.z - this.center.z;

        const cosT = Math.cos(angleRad);
        const sinT = Math.sin(angleRad);

        // Transform point into local space
        const localX = dx * cosT + dz * sinT;
        const localZ = -dx * sinT + dz * cosT;

        // Basic bounds check
        const withinBounds = (
            localX >= -this.halfWidth && localX <= this.halfWidth &&
            localZ >= -this.halfLength && localZ <= this.halfLength
        );

        if (withinBounds && usVelocity) {
            // Transform velocity into local space
            const localVZ = -usVelocity.x * sinT + usVelocity.z * cosT;

            // Check if point is in the "front" half relative to velocity
            const isInFront = localZ * localVZ >= 0;

            return isInFront
        } else return withinBounds
    }

    get corners() {
        const rad = -this.angle * (Math.PI / 180);
        const cos = Math.cos(rad);
        const sin = Math.sin(rad);

        // Corner offsets from center
        const corners = [
            { x: -this.halfWidth, z: -this.halfLength },
            { x: this.halfWidth,  z: -this.halfLength },
            { x: this.halfWidth,  z: this.halfLength },
            { x: -this.halfWidth, z: this.halfLength }
        ];

        // Rotate and translate corners
        return corners.map(corner => {
            return {
                x: this.center.x + corner.x * cos + corner.z * sin,
                z: this.center.z - corner.x * sin + corner.z * cos
            };
        });
    };
    
    get axes() {
        const axes = [];
        for (let i = 0; i < 2; i++) {
            const p1 = this.corners[i];
            const p2 = this.corners[(i + 1) % 4];
            const edge = { x: p2.x - p1.x, z: p2.z - p1.z };
            const normal = { x: -edge.z, z: edge.x }; // perpendicular
            // Normalize
            const length = Math.hypot(normal.x, normal.z);
            axes.push({ x: normal.x / length, z: normal.z / length });
        }
        return axes;
    };

    collidesWith(hitbox) {
        const projectPolygon = (axis, points) => {
            let min = Infinity;
            let max = -Infinity;
            for (const p of points) {
                const proj = p.x * axis.x + p.z * axis.z;
                if (proj < min) min = proj;
                if (proj > max) max = proj;
            }
            return { min, max };
        };
    
        const overlapOnAxis = (axis, corners1, corners2) => {
            const proj1 = projectPolygon(axis, corners1);
            const proj2 = projectPolygon(axis, corners2);
            return !(proj1.max < proj2.min || proj2.max < proj1.min);
        };

        const axes = [...this.axes, ...hitbox.axes];
    
        // SAT: if any axis has no overlap, the rectangles do NOT intersect
        for (const axis of axes) {
            if (!overlapOnAxis(axis, this.corners, hitbox.corners)) {
                return false;
            }
        }
    
        return true; // All projections overlapped
    }
}

function getVectorFromAToB(a, b) {
    const dx = b.x - a.x;
    const dz = b.z - a.z;
    const magnitude = Math.sqrt(dx * dx + dz * dz);
    if (magnitude === 0) return createVector("0 0 0")

    // Return a unit vector pointing from a to b
    return {
        x: dx / magnitude,
        y: 0,
        z: dz / magnitude
    };
}

const collisionProperty = "lb_ta:collision_data"
// this is now only used for making sure new trains don't immediately collide with the player who spawned it.
// originally recordCollision() would have been called every time a collision happened, restarting the cooldown
function collisionDelay(car) {
    if (!car.getDynamicPropertyIds().includes(collisionProperty)) set()

    function set() {
        car.setDynamicProperty(collisionProperty, system.currentTick)
    }

    function get() {
        return car.getDynamicProperty(collisionProperty)
    }

    return {
        canCollide: function() {
            return system.currentTick - get() > 60
        },
        recordCollision: function() {
            return set()
        }
    }
}