import { state } from './state.js';
import { buildCardTransform } from './utils.js';
import { showToast } from './utils.js';
import { saveArrangeHistory } from './cardActions.js';

let BinderManagerRef = null;

const GRAVITY = 2600;           // px/s^2
const RESTITUTION = 0.04;       // 反発係数（厚紙が落ちる程度。ほぼ弾まない）
const REST_MIN_SPEED = 45;      // px/s。これ未満の接近速度では反発させない（微振動対策）
const FRICTION = 0.9;
const LINEAR_DAMPING = 0.35;    // 1秒あたりの速度減衰
const ANGULAR_DAMPING = 1.4;    // 1秒あたりの角速度減衰
const VEL_ITERATIONS = 18;      // 速度ソルバの反復回数
const PEN_SLOP = 0.5;           // この深さまでのめり込みは許容（境界での震え防止）
const POS_CORRECTION = 0.7;     // 1フレームで解消するめり込みの割合
const MAX_CORRECTION = 24;      // px。1フレームあたりの押し戻し上限（一撃で弾き飛ぶのを防ぐ）
const CONTACT_TOL = 1.5;        // この距離まで近ければ「接触あり」とみなす（支持判定用）
const CONTACT_GRACE = 4;        // フレーム
const MAX_SPEED = 4000;         // 数値暴走に対する保険
const MAX_ANG_SPEED = 22;       // rad/s
const SLEEP_LIN_VEL = 7;        // px/s
const SLEEP_ANG_VEL = 0.1;      // rad/s（約5.7deg/s）
const SLEEP_TIME = 0.3;         // 静止候補をこの秒数維持したら眠らせる
const SLEEP_MAX_PEN = 3.5;      // これ以上めり込んでいる間は眠らせない／眠っていても起こす
const SLEEP_TIME_FORCED = 1.6;  // 挟まって解けない場合の保険。この秒数ほぼ動かなければ眠らせる
const MAX_SIM_SECONDS = 8;      // 万一収束しない場合の強制打ち切り
const FIXED_DT = 1 / 60;
const FLOOR_MARGIN = 42;        // 床（ビューポート下端）の内側マージン
const WALL_MARGIN = 6;          // 左右の壁の内側マージン
const QUIET_MOVE = 0.35;        // px/フレーム
const QUIET_ROT = 0.12;         // deg/フレーム
const QUIET_FRAMES = 24;        // 約0.4秒ぶん変化がなければ完了とみなす

function iM(b) { return b.settled ? 0 : b.invM; }
function iI(b) { return b.settled ? 0 : b.invI; }

const STATIC_BODY = { cx: 0, cy: 0, vx: 0, vy: 0, av: 0, invM: 0, invI: 0, settled: true, contacts: 0 };

function axesOf(b) {
    const cos = Math.cos(b.a), sin = Math.sin(b.a);
    return { ux: { x: cos, y: sin }, uy: { x: -sin, y: cos } };
}
function cornersOf(b) {
    const ax = axesOf(b), out = [];
    const sx = [-1, 1, 1, -1], sy = [-1, -1, 1, 1];
    for (let i = 0; i < 4; i++) {
        out.push({
            x: b.cx + ax.ux.x * b.hw * sx[i] + ax.uy.x * b.hh * sy[i],
            y: b.cy + ax.ux.y * b.hw * sx[i] + ax.uy.y * b.hh * sy[i]
        });
    }
    return out;
}

function vdot(a, b) { return a.x * b.x + a.y * b.y; }
function radiusOn(b, n) {
    const ax = axesOf(b);
    return b.hw * Math.abs(vdot(ax.ux, n)) + b.hh * Math.abs(vdot(ax.uy, n));
}
function satContact(A, B) {
    const axA = axesOf(A), axB = axesOf(B);
    const axes = [axA.ux, axA.uy, axB.ux, axB.uy];
    const d = { x: B.cx - A.cx, y: B.cy - A.cy };
    let best = -Infinity, bestN = null;
    for (let i = 0; i < axes.length; i++) {
        const n = axes[i];
        const sep = Math.abs(vdot(d, n)) - (radiusOn(A, n) + radiusOn(B, n));
        if (sep > best) { best = sep; bestN = n; }
    }
    if (best > CONTACT_TOL) return null;
    const normal = vdot(d, bestN) < 0 ? { x: -bestN.x, y: -bestN.y } : bestN;
    return { normal, penetration: -best };
}
function pointInBox(p, b, tol) {
    const ax = axesOf(b), d = { x: p.x - b.cx, y: p.y - b.cy };
    return Math.abs(vdot(d, ax.ux)) <= b.hw + tol && Math.abs(vdot(d, ax.uy)) <= b.hh + tol;
}
function supportPoint(b, dir) {
    const pts = cornersOf(b);
    let best = pts[0], bestD = vdot(pts[0], dir);
    for (let i = 1; i < 4; i++) {
        const dd = vdot(pts[i], dir);
        if (dd > bestD) { bestD = dd; best = pts[i]; }
    }
    return best;
}
function contactPoints(A, B, n) {
    const pts = [];
    cornersOf(A).forEach(p => { if (pointInBox(p, B, CONTACT_TOL)) pts.push(p); });
    cornersOf(B).forEach(p => { if (pointInBox(p, A, CONTACT_TOL)) pts.push(p); });
    if (pts.length === 0) {
        const sa = supportPoint(A, n), sb = supportPoint(B, { x: -n.x, y: -n.y });
        return [{ x: (sa.x + sb.x) / 2, y: (sa.y + sb.y) / 2 }];
    }
    return pts.length > 2 ? pts.slice(0, 2) : pts;
}

let impulseCache = {};

function mkContact(key, a, b, p, n, pen, share) {
    return { key, a, b, p, n, pen, share, jn: 0, jt: 0 };
}

function addStaticContacts(list, b, pts, penOf, n, wallId) {
    const hits = [];
    for (let i = 0; i < pts.length; i++) {
        const pen = penOf(pts[i]);
        if (pen > -CONTACT_TOL) hits.push({ p: pts[i], pen, corner: i });
    }
    if (hits.length === 0) return;
    if (hits.length > 2) {
        hits.sort((x, y) => y.pen - x.pen);
        hits.length = 2;
    }
    b.contacts += hits.length;
    for (let k = 0; k < hits.length; k++) {
        if (hits[k].pen > b.maxPen) b.maxPen = hits[k].pen;
        const key = wallId + b.id + ':' + hits[k].corner;
        list.push(mkContact(key, b, STATIC_BODY, hits[k].p, n, hits[k].pen, 1 / hits.length));
    }
}

function speedOf(b) { return Math.hypot(b.vx, b.vy); }

function buildContacts(sim, floorY, leftX, rightX) {
    const list = [];
    sim.forEach(b => { b.contacts = 0; b.maxPen = 0; });

    sim.forEach(b => {
        const pts = cornersOf(b);
        addStaticContacts(list, b, pts, p => p.y - floorY, { x: 0, y: 1 }, 'F');
        addStaticContacts(list, b, pts, p => leftX - p.x, { x: -1, y: 0 }, 'L');
        addStaticContacts(list, b, pts, p => p.x - rightX, { x: 1, y: 0 }, 'R');
    });

    for (let i = 0; i < sim.length; i++) {
        for (let j = i + 1; j < sim.length; j++) {
            const A = sim[i], B = sim[j];
            const hit = satContact(A, B);
            if (!hit) continue;
            A.contacts++; B.contacts++;
            if (hit.penetration > A.maxPen) A.maxPen = hit.penetration;
            if (hit.penetration > B.maxPen) B.maxPen = hit.penetration;
            const pts = contactPoints(A, B, hit.normal);
            for (let k = 0; k < pts.length; k++) {
                const key = 'P' + A.id + '-' + B.id + ':' + k;
                list.push(mkContact(key, A, B, pts[k], hit.normal, hit.penetration, 1 / pts.length));
            }
        }
    }
    return list;
}

function propagateWake(contacts) {
    let changed = true, guard = 0;
    while (changed && guard++ < 8) {
        changed = false;
        for (let i = 0; i < contacts.length; i++) {
            const ct = contacts[i];
            if (ct.b === STATIC_BODY) continue;
            const A = ct.a, B = ct.b;
            if (A.settled === B.settled) continue;
            const awake = A.settled ? B : A, asleep = A.settled ? A : B;
            if (asleep.jammed) continue; // 膠着して眠ったカードは起こさない
            if (awake.maxPen > SLEEP_MAX_PEN) {
                asleep.settled = false;
                asleep.restTimer = 0;
                asleep.slowTimer = 0;
                changed = true;
            }
        }
    }
}

function relVelAt(ct) {
    const A = ct.a, B = ct.b;
    const vax = A.vx - A.av * ct.ray, vay = A.vy + A.av * ct.rax;
    const vbx = B.vx - B.av * ct.rby, vby = B.vy + B.av * ct.rbx;
    return { x: vbx - vax, y: vby - vay };
}
function applyImpulseAt(ct, px, py) {
    const A = ct.a, B = ct.b;
    const imA = iM(A), imB = iM(B), iiA = iI(A), iiB = iI(B);
    A.vx -= px * imA; A.vy -= py * imA;
    A.av -= iiA * (ct.rax * py - ct.ray * px);
    B.vx += px * imB; B.vy += py * imB;
    B.av += iiB * (ct.rbx * py - ct.rby * px);
}

function prepareContact(ct) {
    const A = ct.a, B = ct.b, n = ct.n;
    ct.rax = ct.p.x - A.cx; ct.ray = ct.p.y - A.cy;
    ct.rbx = ct.p.x - B.cx; ct.rby = ct.p.y - B.cy;
    ct.t = { x: -n.y, y: n.x };

    const imA = iM(A), imB = iM(B), iiA = iI(A), iiB = iI(B);
    const rnA = ct.rax * n.y - ct.ray * n.x, rnB = ct.rbx * n.y - ct.rby * n.x;
    ct.kn = imA + imB + iiA * rnA * rnA + iiB * rnB * rnB;
    const rtA = ct.rax * ct.t.y - ct.ray * ct.t.x, rtB = ct.rbx * ct.t.y - ct.rby * ct.t.x;
    ct.kt = imA + imB + iiA * rtA * rtA + iiB * rtB * rtB;

    const vn0 = ct.kn > 0 ? (() => { const rv = relVelAt(ct); return rv.x * n.x + rv.y * n.y; })() : 0;
    ct.bias = (-vn0 > REST_MIN_SPEED) ? RESTITUTION * (-vn0) : 0;

    const prev = impulseCache[ct.key];
    ct.jn = prev ? prev.jn : 0;
    ct.jt = prev ? prev.jt : 0;
    if (ct.jn !== 0 || ct.jt !== 0) {
        applyImpulseAt(ct, ct.jn * n.x + ct.jt * ct.t.x, ct.jn * n.y + ct.jt * ct.t.y);
    }
}

function solveVelocity(ct) {
    if (ct.kn <= 0) return;
    const n = ct.n, t = ct.t;

    let rv = relVelAt(ct);
    const vn = rv.x * n.x + rv.y * n.y;
    let jn = -(vn + ct.bias) / ct.kn;
    const totalN = Math.max(ct.jn + jn, 0); // 累積インパルスでクランプ（引き寄せる向きは出さない）
    jn = totalN - ct.jn;
    ct.jn = totalN;
    applyImpulseAt(ct, jn * n.x, jn * n.y);

    if (ct.kt <= 0) return;
    rv = relVelAt(ct);
    const vt = rv.x * t.x + rv.y * t.y;
    let jt = -vt / ct.kt;
    const maxF = FRICTION * ct.jn;
    const totalT = Math.max(-maxF, Math.min(maxF, ct.jt + jt));
    jt = totalT - ct.jt;
    ct.jt = totalT;
    applyImpulseAt(ct, jt * t.x, jt * t.y);
}

function solvePosition(ct) {
    const imA = iM(ct.a), imB = iM(ct.b);
    const sum = imA + imB;
    if (sum <= 0) return;
    const depth = Math.min(Math.max(ct.pen - PEN_SLOP, 0), MAX_CORRECTION);
    if (depth <= 0) return;
    const mag = depth / sum * POS_CORRECTION * ct.share;
    ct.a.cx -= mag * ct.n.x * imA; ct.a.cy -= mag * ct.n.y * imA;
    ct.b.cx += mag * ct.n.x * imB; ct.b.cy += mag * ct.n.y * imB;
}

function simStep(sim, dt, floorY, leftX, rightX) {
    const linDamp = 1 / (1 + LINEAR_DAMPING * dt);
    const angDamp = 1 / (1 + ANGULAR_DAMPING * dt);

    sim.forEach(b => {
        b.prevCx = b.cx; b.prevCy = b.cy; b.prevA = b.a; // 1ステップで実際に動いた量の測定用
        if (b.settled) return;
        b.vy += GRAVITY * dt;
        b.vx *= linDamp; b.vy *= linDamp; b.av *= angDamp;
    });

    const contacts = buildContacts(sim, floorY, leftX, rightX);
    propagateWake(contacts); // 有効質量が変わるので、必ず下ごしらえより前に済ませる
    for (let i = 0; i < contacts.length; i++) prepareContact(contacts[i]);

    for (let it = 0; it < VEL_ITERATIONS; it++) {
        for (let i = 0; i < contacts.length; i++) solveVelocity(contacts[i]);
    }

    const nextCache = {};
    for (let i = 0; i < contacts.length; i++) nextCache[contacts[i].key] = { jn: contacts[i].jn, jt: contacts[i].jt };
    impulseCache = nextCache;

    sim.forEach(b => {
        if (b.settled) return;
        const sp = Math.hypot(b.vx, b.vy);
        if (sp > MAX_SPEED) { b.vx *= MAX_SPEED / sp; b.vy *= MAX_SPEED / sp; }
        if (Math.abs(b.av) > MAX_ANG_SPEED) b.av = (b.av < 0 ? -1 : 1) * MAX_ANG_SPEED;
        b.cx += b.vx * dt;
        b.cy += b.vy * dt;
        b.a += b.av * dt;
    });

    for (let k = 0; k < contacts.length; k++) solvePosition(contacts[k]);

    sim.forEach(b => {
        b.airFrames = b.contacts > 0 ? 0 : b.airFrames + 1;

        if (b.settled) {
            if (b.airFrames >= CONTACT_GRACE) {
                b.settled = false; b.jammed = false; b.restTimer = 0; b.slowTimer = 0;
            }
            return;
        }

        const supported = b.airFrames < CONTACT_GRACE;
        const slow = speedOf(b) < SLEEP_LIN_VEL && Math.abs(b.av) < SLEEP_ANG_VEL;
        if (supported && slow && b.maxPen < SLEEP_MAX_PEN) b.restTimer += dt;
        else b.restTimer = 0;

        const moved = Math.hypot(b.cx - b.prevCx, b.cy - b.prevCy);
        const turned = Math.abs(b.a - b.prevA) * 180 / Math.PI;
        if (supported && moved < QUIET_MOVE && turned < QUIET_ROT) b.slowTimer += dt;
        else b.slowTimer = 0;

        if (b.restTimer > SLEEP_TIME) {
            b.settled = true; b.jammed = false;
            b.vx = 0; b.vy = 0; b.av = 0;
        } else if (b.slowTimer > SLEEP_TIME_FORCED) {
            b.settled = true; b.jammed = true;
            b.vx = 0; b.vy = 0; b.av = 0;
        }
    });
}


function rand(min, max) { return min + Math.random() * (max - min); }

function computeWorldBounds() {
    const bc = document.getElementById('binder-container');
    const rect = bc ? bc.getBoundingClientRect() : { width: window.innerWidth, height: window.innerHeight };
    const scale = BinderManagerRef ? BinderManagerRef.getScale() : 1;
    const translate = BinderManagerRef ? BinderManagerRef.getTranslate() : { x: 0, y: 0 };
    const worldLeft = -translate.x / scale;
    const worldTop = -translate.y / scale;
    const worldWidth = rect.width / scale;
    const worldHeight = rect.height / scale;
    return {
        floorY: worldTop + worldHeight - FLOOR_MARGIN,
        leftX: worldLeft + WALL_MARGIN,
        rightX: worldLeft + worldWidth - WALL_MARGIN
    };
}

function renderBody(b) {
    const x = b.cx - b.hw, y = b.cy - b.hh, rotDeg = b.a * 180 / Math.PI;
    b.el.dataset.x = x;
    b.el.dataset.y = y;
    b.el.dataset.rotation = rotDeg;
    b.el.style.setProperty('--card-rotation', `${rotDeg}deg`);
    b.el.style.transform = buildCardTransform(x, y, rotDeg);
}

function commitBody(b) {
    if (!b.placement) return;
    b.placement.x = b.cx - b.hw;
    b.placement.y = b.cy - b.hh;
    b.placement.rotation = b.a * 180 / Math.PI;
}

let dropping = false;
let currentRunId = 0;

export async function dropGravity() {
    if (state.gravityLocked || dropping) return;
    if (!state.currentBinder || !state.currentBinder.placements) return;

    const placements = state.currentBinder.placements;
    const cardPlacements = placements.filter(p => p.type === 'card' || p.type === 'binder');

    const cardElById = new Map();
    document.querySelectorAll('.binder-card[data-id]').forEach(el => cardElById.set(el.dataset.id, el));

    const dropTargets = cardPlacements.filter(p => cardElById.has(String(p.id)));
    if (dropTargets.length === 0) return;

    dropping = true;
    const runId = ++currentRunId;

    saveArrangeHistory(dropTargets);

    state.gravityLocked = true;
    document.body.classList.add('gravity-locked');
    if (BinderManagerRef && BinderManagerRef.clearSelection) BinderManagerRef.clearSelection();

    let { floorY, leftX, rightX } = computeWorldBounds();
    impulseCache = {};

    const sim = dropTargets.map(p => {
        const el = cardElById.get(String(p.id));
        const w = el.offsetWidth > 0 ? el.offsetWidth : (p.width || 220);
        const h = el.offsetHeight > 0 ? el.offsetHeight : (p.height || 140);
        const x = parseFloat(el.dataset.x || p.x || 0);
        const y = parseFloat(el.dataset.y || p.y || 0);
        const rotDeg = parseFloat(el.dataset.rotation || p.rotation || 0);
        const mass = (w * h) / 15000;
        return {
            id: String(p.id), el, placement: p,
            hw: w / 2, hh: h / 2,
            cx: x + w / 2, cy: y + h / 2,
            a: rotDeg * Math.PI / 180,
            vx: 0, vy: 0,
            av: rand(-1.2, 1.2), // rad/s。落下開始時のわずかなばらつき
            invM: 1 / mass,
            invI: 1 / (mass * (w * w + h * h) / 12),
            settled: false, jammed: false,
            restTimer: 0, slowTimer: 0, contacts: 0, airFrames: 0, maxPen: 0
        };
    });

    let maxCardBottom = -Infinity, minCardLeft = Infinity, maxCardRight = -Infinity;
    sim.forEach(b => {
        maxCardBottom = Math.max(maxCardBottom, b.cy + b.hh);
        minCardLeft = Math.min(minCardLeft, b.cx - b.hw);
        maxCardRight = Math.max(maxCardRight, b.cx + b.hw);
    });
    if (maxCardBottom > floorY) floorY = maxCardBottom + 1;
    if (minCardLeft < leftX) leftX = minCardLeft - 1;
    if (maxCardRight > rightX) rightX = maxCardRight + 1;

    const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    function renderAll() {
        let moved = false;
        sim.forEach(b => {
            const dx = Math.abs((b.cx - b.hw) - parseFloat(b.el.dataset.x || 0));
            const dy = Math.abs((b.cy - b.hh) - parseFloat(b.el.dataset.y || 0));
            const dr = Math.abs((b.a * 180 / Math.PI) - parseFloat(b.el.dataset.rotation || 0));
            if (dx > QUIET_MOVE || dy > QUIET_MOVE || dr > QUIET_ROT) moved = true;
            renderBody(b);
        });
        return moved;
    }

    function finish() {
        sim.forEach(b => { b.settled = true; commitBody(b); renderBody(b); });
        dropping = false;
        showToast('……何かが崩れ落ちた音がした。右クリック→「整列をなかったことに」で戻せます', 'info');
    }

    if (reduced) {
        const maxSteps = Math.round(MAX_SIM_SECONDS / FIXED_DT);
        for (let n = 0; n < maxSteps && sim.some(s => !s.settled); n++) {
            simStep(sim, FIXED_DT, floorY, leftX, rightX);
        }
        finish();
        return;
    }

    let startTime = null;
    let quietFrames = 0;
    function frame(now) {
        if (runId !== currentRunId) return;
        if (startTime === null) startTime = now;
        simStep(sim, FIXED_DT, floorY, leftX, rightX);
        quietFrames = renderAll() ? 0 : quietFrames + 1;

        const elapsed = (now - startTime) / 1000;
        const allSettled = sim.every(s => s.settled) || quietFrames >= QUIET_FRAMES;
        if (!allSettled && elapsed < MAX_SIM_SECONDS) {
            requestAnimationFrame(frame);
        } else {
            finish();
        }
    }
    requestAnimationFrame(frame);
}

const TRIGGER_WORD = 'gravity';
const KEY_TIMEOUT_MS = 1500; // 1文字ごとの間隔がこれを超えたらバッファをリセットする
let typedBuffer = '';
let lastKeyTime = 0;

function handleTriggerKeydown(e) {
    if (state.gravityLocked || dropping) return;
    if (!state.currentBinder) return;

    const tag = (e.target && e.target.tagName || '').toLowerCase();
    if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
    if (document.querySelector('.modal-overlay.active')) return;
    if (e.key.length !== 1 || !/[a-z]/i.test(e.key)) return;

    const now = performance.now();
    if (now - lastKeyTime > KEY_TIMEOUT_MS) typedBuffer = '';
    lastKeyTime = now;

    typedBuffer = (typedBuffer + e.key.toLowerCase()).slice(-TRIGGER_WORD.length);

    if (typedBuffer === TRIGGER_WORD) {
        typedBuffer = '';
        dropGravity();
    }
}

export function cancelGravity() {
    currentRunId++; // 実行中のrAFループがあれば次のフレームで静かに止まる
    dropping = false;
    if (state.gravityLocked) {
        state.gravityLocked = false;
        document.body.classList.remove('gravity-locked');
    }
}

export function initGravity(BinderManager) {
    BinderManagerRef = BinderManager;
    window.addEventListener('keydown', handleTriggerKeydown);
}
