// Exact Haxball physics engine
// Based on reference implementation from Wazarr94
// Step order: input → kick → position += speed → speed *= damping → collisions

class PhysicsEngine {
    constructor(stadium) {
        this.stadium = stadium;
        this.staticDiscs = [];
        this.isKickoff = false;

        // Initialize static discs (goalposts etc) from stadium with trait merging
        for (const d of stadium.discs) {
            // Merge trait properties
            const props = {};
            if (d.trait && stadium.traits && stadium.traits[d.trait]) {
                Object.assign(props, stadium.traits[d.trait]);
            }
            Object.assign(props, d); // disc-level props override traits

            this.staticDiscs.push({
                x: props.pos[0], y: props.pos[1],
                vx: 0, vy: 0,
                radius: props.radius || 10,
                invMass: props.invMass !== undefined ? props.invMass : 0,
                bCoef: props.bCoef !== undefined ? props.bCoef : 0.5,
                damping: props.damping !== undefined ? props.damping : 0.99,
                color: props.color,
                cMask: this.resolveCollisionFlags(props.cMask || ["all"]),
                cGroup: this.resolveCollisionFlags(props.cGroup || ["all"])
            });
        }


        // Pre-resolve collision flags for segments, vertexes, planes
        this.resolvedSegments = stadium.segments.map(seg => {
            const v0 = stadium.vertexes[seg.v0];
            const v1 = stadium.vertexes[seg.v1];
            // Merge trait props
            const trait = this.getTrait(seg, stadium);
            return {
                v0: [v0.x, v0.y],
                v1: [v1.x, v1.y],
                bCoef: seg.bCoef !== undefined ? seg.bCoef : (trait.bCoef !== undefined ? trait.bCoef : 1),
                curve: seg.curve || trait.curve || 0,
                cMask: this.resolveCollisionFlags(seg.cMask || trait.cMask || ["all"]),
                cGroup: this.resolveCollisionFlags(seg.cGroup || trait.cGroup || ["wall"]),
                isKickOff: (v0.trait === 'kickOffBarrier') || false,
                // Pre-compute segment geometry
                ...this.computeSegmentGeometry(v0, v1, seg.curve || trait.curve || 0)
            };
        });

        this.resolvedVertexes = stadium.vertexes.map(v => {
            const trait = this.getVertexTrait(v, stadium);
            return {
                x: v.x, y: v.y,
                bCoef: v.bCoef !== undefined ? v.bCoef : (trait.bCoef !== undefined ? trait.bCoef : 1),
                cMask: this.resolveCollisionFlags(v.cMask || trait.cMask || ["all"]),
                cGroup: this.resolveCollisionFlags(v.cGroup || trait.cGroup || ["wall"]),
                isKickOff: (v.trait === 'kickOffBarrier') || false
            };
        });

        this.resolvedPlanes = stadium.planes.map(p => {
            const trait = this.getPlaneTrait(p, stadium);
            return {
                normal: p.normal,
                dist: p.dist,
                bCoef: p.bCoef !== undefined ? p.bCoef : (trait.bCoef !== undefined ? trait.bCoef : 1),
                cMask: this.resolveCollisionFlags(p.cMask || trait.cMask || ["all"]),
                cGroup: this.resolveCollisionFlags(p.cGroup || trait.cGroup || ["wall"])
            };
        });
    }

    // Collision flag constants (matching Haxball)
    static FLAGS = {
        ball: 1, red: 2, blue: 4, redKO: 8, blueKO: 16,
        wall: 32, all: 63, kick: 64, score: 128
    };

    resolveCollisionFlags(flags) {
        if (typeof flags === 'number') return flags;
        if (!Array.isArray(flags)) return 63;
        let result = 0;
        for (const f of flags) {
            if (PhysicsEngine.FLAGS[f] !== undefined) {
                result |= PhysicsEngine.FLAGS[f];
            }
        }
        return result;
    }

    getTrait(seg, stadium) {
        const v0 = stadium.vertexes[seg.v0];
        if (v0.trait && stadium.traits && stadium.traits[v0.trait]) {
            return stadium.traits[v0.trait];
        }
        return {};
    }

    getVertexTrait(v, stadium) {
        if (v.trait && stadium.traits && stadium.traits[v.trait]) {
            return stadium.traits[v.trait];
        }
        return {};
    }

    getPlaneTrait(p, stadium) {
        if (p.trait && stadium.traits && stadium.traits[p.trait]) {
            return stadium.traits[p.trait];
        }
        return {};
    }

    computeSegmentGeometry(v0, v1, curveDeg) {
        if (curveDeg === 0) {
            // Straight segment - compute normal
            const dx = v0.x - v1.x;
            const dy = -(v0.y - v1.y);
            const len = Math.sqrt(dx * dx + dy * dy);
            if (len === 0) return { segType: 'straight', normal: [0, 0] };
            return { segType: 'straight', normal: [dy / len, dx / len] };
        } else {
            // Curved segment
            let curve = curveDeg * 0.017453292519943295;
            let sv0 = [v0.x, v0.y];
            let sv1 = [v1.x, v1.y];
            if (curve < 0) {
                curve = -curve;
                const tmp = sv0;
                sv0 = sv1;
                sv1 = tmp;
            }
            const liminf = 0.17435839227423353;
            const limsup = 5.934119456780721;
            if (curve <= liminf || curve >= limsup) {
                return { segType: 'invalid' };
            }
            const curveF = 1 / Math.tan(curve / 2);
            const cx = 0.5 * (sv1[0] - sv0[0]);
            const cy = 0.5 * (sv1[1] - sv0[1]);
            const circleCenter = [sv0[0] + cx + (-cy) * curveF, sv0[1] + cy + cx * curveF];
            const rx = sv0[0] - circleCenter[0];
            const ry = sv0[1] - circleCenter[1];
            const circleRadius = Math.sqrt(rx * rx + ry * ry);
            const v0Tan = [-(sv0[1] - circleCenter[1]), sv0[0] - circleCenter[0]];
            const v1Tan = [-(circleCenter[1] - sv1[1]), circleCenter[0] - sv1[0]];
            if (curveF <= 0) {
                v0Tan[0] *= -1; v0Tan[1] *= -1;
                v1Tan[0] *= -1; v1Tan[1] *= -1;
            }
            return { segType: 'curve', curveF, circleCenter, circleRadius, v0Tan, v1Tan, sv0, sv1 };
        }
    }

    createBall() {
        const bp = this.stadium.ballPhysics;
        return {
            x: 0, y: 0, vx: 0, vy: 0,
            radius: bp.radius,
            invMass: bp.invMass,
            bCoef: bp.bCoef,
            damping: bp.damping,
            color: bp.color,
            cMask: this.resolveCollisionFlags(bp.cMask),
            cGroup: this.resolveCollisionFlags(bp.cGroup) | PhysicsEngine.FLAGS.score | PhysicsEngine.FLAGS.kick,
            isBall: true
        };
    }

    createPlayer(team, spawnIndex) {
        const pp = this.stadium.playerPhysics;
        const spawns = team === 'red' ? this.stadium.redSpawnPoints : this.stadium.blueSpawnPoints;
        const spawn = spawns[spawnIndex % spawns.length];
        const teamCGroup = team === 'red' ? PhysicsEngine.FLAGS.red : PhysicsEngine.FLAGS.blue;
        const kickOffFlag = team === 'red' ? PhysicsEngine.FLAGS.redKO : PhysicsEngine.FLAGS.blueKO;
        return {
            x: spawn.x, y: spawn.y, vx: 0, vy: 0,
            radius: pp.radius,
            invMass: pp.invMass,
            bCoef: pp.bCoef,
            damping: pp.damping,
            acceleration: pp.acceleration,
            kickingAcceleration: pp.kickingAcceleration,
            kickingDamping: pp.kickingDamping,
            kickStrength: pp.kickStrength,
            color: team === 'red' ? 'E56E56' : '5689E5',
            cMask: 39 | kickOffFlag, // all + kick + team KO
            cGroup: teamCGroup,
            team,
            input: { up: false, down: false, left: false, right: false, kick: false },
            isKicking: false,
            shotReset: false,
            kickTimer: 0
        };
    }

    // Main physics step - exact Haxball order
    step(gameState) {
        const { discs } = gameState;

        // 1. Resolve player movement + kick detection
        let anyKicked = false;
        for (const disc of discs) {
            if (disc.input) {
                if (this.resolvePlayerMovement(disc, discs)) {
                    anyKicked = true;
                }
            }
        }

        // 2. Position update: x += xspeed
        for (const disc of discs) {
            disc.x += disc.vx;
            disc.y += disc.vy;
        }

        // 3. Damping: xspeed *= damping (AFTER position update, like Haxball)
        for (const disc of discs) {
            const damping = (disc.input && disc.isKicking) ? disc.kickingDamping : disc.damping;
            disc.vx *= damping;
            disc.vy *= damping;
        }

        // 4. Collisions
        // Disc vs Disc
        for (let i = 0; i < discs.length; i++) {
            for (let j = i + 1; j < discs.length; j++) {
                if ((discs[i].cGroup & discs[j].cMask) !== 0 && (discs[i].cMask & discs[j].cGroup) !== 0) {
                    this.resolveDDCollision(discs[i], discs[j]);
                }
            }
        }

        // Disc vs Static Discs (goalposts)
        for (const disc of discs) {
            if (disc.invMass === 0) continue;
            for (const sd of this.staticDiscs) {
                if ((disc.cGroup & sd.cMask) !== 0 && (disc.cMask & sd.cGroup) !== 0) {
                    this.resolveDDCollision(disc, sd);
                }
            }
        }

        // Disc vs Planes
        for (const disc of discs) {
            if (disc.invMass === 0) continue;
            for (const plane of this.resolvedPlanes) {
                if ((disc.cGroup & plane.cMask) !== 0 && (disc.cMask & plane.cGroup) !== 0) {
                    this.resolveDPCollision(disc, plane);
                }
            }
        }

        // Disc vs Segments
        for (const disc of discs) {
            if (disc.invMass === 0) continue;
            for (const seg of this.resolvedSegments) {
                // Skip kickoff barriers when not in kickoff
                if (seg.isKickOff && !this.isKickoff) continue;
                if ((disc.cGroup & seg.cMask) !== 0 && (disc.cMask & seg.cGroup) !== 0) {
                    this.resolveDSCollision(disc, seg);
                }
            }
        }

        // Disc vs Vertexes
        for (const disc of discs) {
            if (disc.invMass === 0) continue;
            for (const v of this.resolvedVertexes) {
                if (v.isKickOff && !this.isKickoff) continue;
                if ((disc.cGroup & v.cMask) !== 0 && (disc.cMask & v.cGroup) !== 0) {
                    this.resolveDVCollision(disc, v);
                }
            }
        }

        // Update kickoff mask
        if (this.isKickoff) {
            for (const disc of discs) {
                if (disc.input) {
                    const kickOffFlag = disc.team === 'red' ? PhysicsEngine.FLAGS.redKO : PhysicsEngine.FLAGS.blueKO;
                    disc.cMask = 39 | kickOffFlag;
                }
            }
        } else {
            for (const disc of discs) {
                if (disc.input) {
                    disc.cMask = 39; // Remove KO flags during play
                }
            }
        }

        // Handle kick timer
        for (const disc of discs) {
            if (disc.kickTimer !== undefined && disc.kickTimer > 0) disc.kickTimer--;
            if (disc.kickTimer !== undefined && disc.kickTimer <= 0 && disc.input && !disc.input.kick) {
                disc.isKicking = false;
            }
        }

        return {
            goal: this.checkGoals(discs),
            kicked: anyKicked
        };
    }

    resolvePlayerMovement(disc, allDiscs) {
        const input = disc.input;

        // Shooting state
        if (input.kick) {
            disc.isKicking = true;
        } else {
            disc.isKicking = false;
            disc.shotReset = false;
        }

        // Kick detection (exact Haxball formula: dist - r1 - r2 < 4)
        if (disc.isKicking && !disc.shotReset) {
            let kicked = false;
            for (const d of allDiscs) {
                if (d === disc) continue;
                // Only kick discs with 'kick' collision flag (the ball)
                if ((d.cGroup & PhysicsEngine.FLAGS.kick) === 0) continue;

                const dx = d.x - disc.x;
                const dy = d.y - disc.y;
                const dist = Math.sqrt(dx * dx + dy * dy);
                // Exact Haxball: dist - playerRadius - ballRadius < 4
                if (dist - disc.radius - d.radius < 4) {
                    const nx = dx / dist;
                    const ny = dy / dist;
                    d.vx += nx * disc.kickStrength;
                    d.vy += ny * disc.kickStrength;
                    kicked = true;
                }
            }
            if (kicked) {
                disc.shotReset = true;
                disc.kickTimer = 8;
                return true;
            }
        }

        // Movement input
        let ax = 0, ay = 0;
        if (input.up) ay -= 1;
        if (input.down) ay += 1;
        if (input.left) ax -= 1;
        if (input.right) ax += 1;

        // Normalize diagonal
        const len = Math.sqrt(ax * ax + ay * ay);
        if (len > 0) {
            ax /= len;
            ay /= len;
        }

        const accel = disc.isKicking ? disc.kickingAcceleration : disc.acceleration;
        disc.vx += ax * accel;
        disc.vy += ay * accel;

        return false;
    }

    // Exact Haxball disc-disc collision
    resolveDDCollision(discA, discB) {
        const dx = discA.x - discB.x;
        const dy = discA.y - discB.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        const radiusSum = discA.radius + discB.radius;

        if (dist <= 0 || dist > radiusSum) return;

        const normal = [dx / dist, dy / dist];
        const totalInvMass = discA.invMass + discB.invMass;
        if (totalInvMass === 0) return;
        const massFactor = discA.invMass / totalInvMass;

        // Position correction
        const overlap = radiusSum - dist;
        discA.x += normal[0] * overlap * massFactor;
        discA.y += normal[1] * overlap * massFactor;
        discB.x -= normal[0] * overlap * (1 - massFactor);
        discB.y -= normal[1] * overlap * (1 - massFactor);

        // Velocity resolution
        const relVx = discA.vx - discB.vx;
        const relVy = discA.vy - discB.vy;
        const normalVelocity = relVx * normal[0] + relVy * normal[1];

        if (normalVelocity < 0) {
            // Exact Haxball bCoef formula: multiplicative
            const speedFactor = normalVelocity * (discA.bCoef * discB.bCoef + 1);
            discA.vx -= normal[0] * speedFactor * massFactor;
            discA.vy -= normal[1] * speedFactor * massFactor;
            discB.vx += normal[0] * speedFactor * (1 - massFactor);
            discB.vy += normal[1] * speedFactor * (1 - massFactor);
            return true;
        }

        return false;
    }

    // Exact Haxball plane collision
    resolveDPCollision(disc, plane) {
        const norm = this.normalise(plane.normal);
        const dist = plane.dist - (norm[0] * disc.x + norm[1] * disc.y) + disc.radius;

        if (dist > 0) {
            disc.x += norm[0] * dist;
            disc.y += norm[1] * dist;
            const speedCoef = norm[0] * disc.vx + norm[1] * disc.vy;
            if (speedCoef < 0) {
                const factor = speedCoef * (disc.bCoef * plane.bCoef + 1);
                disc.vx -= norm[0] * factor;
                disc.vy -= norm[1] * factor;
            }
        }
    }

    // Exact Haxball segment collision
    resolveDSCollision(disc, seg) {
        let distance, coefX, coefY;

        if (seg.segType === 'straight') {
            const v0 = seg.v0;
            const v1 = seg.v1;
            const segX = v1[0] - v0[0];
            const segY = v1[1] - v0[1];
            const d0x = disc.x - v0[0];
            const d0y = disc.y - v0[1];
            const d1x = disc.x - v1[0];
            const d1y = disc.y - v1[1];

            // Check projection bounds
            if (d0x * segX + d0y * segY <= 0 || d1x * segX + d1y * segY >= 0) return;

            const norm = this.normalise([v0[1] - v1[1], v1[0] - v0[0]]);
            coefX = -norm[0];
            coefY = -norm[1];
            distance = coefX * d1x + coefY * d1y;
        } else if (seg.segType === 'curve') {
            const dx = disc.x - seg.circleCenter[0];
            const dy = disc.y - seg.circleCenter[1];
            // Check tangent bounds
            if ((seg.v1Tan[0] * dx + seg.v1Tan[1] * dy > 0 && seg.v0Tan[0] * dx + seg.v0Tan[1] * dy > 0) === (seg.curveF < 0)) {
                return;
            }
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist === 0) return;
            distance = dist - seg.circleRadius;
            coefX = dx / dist;
            coefY = dy / dist;
        } else {
            return;
        }

        // Handle bias (0 or undefined = two-sided)
        if (distance < 0) {
            distance = -distance;
            coefX = -coefX;
            coefY = -coefY;
        }

        if (distance < disc.radius) {
            const penetration = disc.radius - distance;
            disc.x += coefX * penetration;
            disc.y += coefY * penetration;
            const speedCoef = coefX * disc.vx + coefY * disc.vy;
            if (speedCoef < 0) {
                const factor = speedCoef * (disc.bCoef * seg.bCoef + 1);
                disc.vx -= coefX * factor;
                disc.vy -= coefY * factor;
            }
        }
    }

    // Exact Haxball vertex collision
    resolveDVCollision(disc, vertex) {
        const dx = disc.x - vertex.x;
        const dy = disc.y - vertex.y;
        const distSq = dx * dx + dy * dy;
        if (distSq <= 0 || distSq > disc.radius * disc.radius) return;
        const dist = Math.sqrt(distSq);
        const nx = dx / dist;
        const ny = dy / dist;
        const penetration = disc.radius - dist;
        disc.x += nx * penetration;
        disc.y += ny * penetration;
        const speedCoef = nx * disc.vx + ny * disc.vy;
        if (speedCoef < 0) {
            const factor = speedCoef * (disc.bCoef * vertex.bCoef + 1);
            disc.vx -= nx * factor;
            disc.vy -= ny * factor;
        }
    }

    normalise(vec) {
        const len = Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]);
        if (len === 0) return [0, 0];
        return [vec[0] / len, vec[1] / len];
    }

    checkGoals(discs) {
        const ball = discs.find(d => d.isBall);
        if (!ball) return null;

        for (const goal of this.stadium.goals) {
            const [x0, y0] = goal.p0;
            const [x1, y1] = goal.p1;
            const goalX = x0;

            if (goal.team === 'red') {
                if (ball.x < goalX && ball.y > Math.min(y0, y1) && ball.y < Math.max(y0, y1)) {
                    return { scoringTeam: 'blue' };
                }
            } else {
                if (ball.x > goalX && ball.y > Math.min(y0, y1) && ball.y < Math.max(y0, y1)) {
                    return { scoringTeam: 'red' };
                }
            }
        }
        return null;
    }
}

module.exports = { PhysicsEngine };
