/*  guarda.jsx — a GUARDA de layout do motion (ES3: só var e function).

    O mesmo arquivo roda em dois lugares, ANTES do render:
      1. no Node (lib/estudio/maquina-guarda.js carrega pelo vm): planeja a cena "foto" (foto, título, ícones) com a largura
         medida da fonte e escreve o layout no plano;
      2. no kit (o maquina.js concatena depois do base.jsx): no fecha() de CADA cena, com as caixas reais (sourceRectAtTime),
         conserta o que o modelo ou o diretor puseram torto.

    Regras (a régua do dono: muito movimento, mas NADA feio — nada gigante, nada sobreposto, nada colado na borda):
      tamanho   texto até 88% da largura; ícone entre 12% e 24% do lado menor; foto entre 55% e 80% da largura no 9:16
      zona      tudo dentro de 6% de margem; no 9:16 fora das faixas de interface do Reels/TikTok (10% em cima, 20% embaixo)
                e do trilho de botões da direita
      sobrepor  texto não encosta em nada (folga mínima); ícone nunca sobre texto, foto/rosto ou outro ícone
      lotação   no máximo N elementos ao mesmo tempo (os ícones de menor prioridade saem)
    Quando viola, CONSERTA (reposiciona, encolhe, tira o ícone de trás); se não der, pede a troca da peça (trocar: true).
    Cada conserto vira uma linha do log da cena.

    item = { id, tipo: 'texto'|'icone'|'foto'|'cartao'|'cursor', c: [x0, y0, x1, y1] (o envelope de todos os quadros-chave),
             fixo (não move nem encolhe), prio (maior fica), nome, alvo (cursor: a caixa do botão que ele clica) }
    cursor    a setinha é elemento: nunca em cima de letra; clica na borda do botão ou depois do texto (gCursor)
*/

var GUARDA = { margem: 0.06, folga: 28, maxSimult: 6, textoMaxL: 0.88, iconeMin: 0.12, iconeMax: 0.24, fotoMin: 0.55, fotoMax: 0.8, passo: 0.025 };

function gAlto(W, H) { return H / W > 1.6; }
/* A zona segura (no mesmo espaço das caixas). o.legenda: a legenda sobe a faixa de baixo. */
function gZona(W, H, o) {
    o = o || {};
    var m = GUARDA.margem, z = { x0: W * m, x1: W * (1 - m), y0: H * m, y1: H * (1 - m), trilho: null };
    if (gAlto(W, H)) {
        z.y0 = H * 0.1; z.y1 = H * (o.legenda ? 0.73 : 0.8);
        z.trilho = [W * 0.885, H * 0.5, W, H];   // a coluna de curtir/comentar/compartilhar
    }
    return z;
}
function gW(c) { return c[2] - c[0]; }
function gH(c) { return c[3] - c[1]; }
function gCx(c) { return (c[0] + c[2]) / 2; }
function gCy(c) { return (c[1] + c[3]) / 2; }
function gR(n) { return Math.round(n); }
function gTxt(c) { return '[' + gR(c[0]) + ',' + gR(c[1]) + ',' + gR(c[2]) + ',' + gR(c[3]) + ']'; }
function gCaixa(cx, cy, w, h) { return [cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2]; }
function gMove(c, dx, dy) { return [c[0] + dx, c[1] + dy, c[2] + dx, c[3] + dy]; }
function gEscala(c, k) { return gCaixa(gCx(c), gCy(c), gW(c) * k, gH(c) * k); }
/* Encostam? (com folga m entre as duas) */
function gToca(a, b, m) { m = m || 0; return a[0] < b[2] + m && b[0] < a[2] + m && a[1] < b[3] + m && b[1] < a[3] + m; }
function gCabe(c, z) { return c[0] >= z.x0 - 0.5 && c[2] <= z.x1 + 0.5 && c[1] >= z.y0 - 0.5 && c[3] <= z.y1 + 0.5; }
function gNoTrilho(c, z) { return !!z.trilho && gToca(c, z.trilho, 0); }
function gNome(it) { return it.nome || (it.tipo + ' ' + it.id); }

/* Põe a caixa dentro da zona (encolhe se for maior). Devolve a caixa nova. */
function gEncaixa(c, z) {
    var w = gW(c), h = gH(c), zw = z.x1 - z.x0, zh = z.y1 - z.y0, k = Math.min(1, zw / w, zh / h);
    if (k < 1) c = gEscala(c, k);
    var dx = 0, dy = 0;
    if (c[0] < z.x0) dx = z.x0 - c[0]; else if (c[2] > z.x1) dx = z.x1 - c[2];
    if (c[1] < z.y0) dy = z.y0 - c[1]; else if (c[3] > z.y1) dy = z.y1 - c[3];
    return gMove(c, dx, dy);
}

/* Procura um lugar livre do tamanho (w, h) o mais perto de (cx, cy): nada em `ocupado` (com folga), dentro da zona, fora do trilho. */
function gLugarLivre(cx, cy, w, h, z, ocupado, folga, W) {
    var passo = Math.max(8, W * GUARDA.passo), melhor = null, md = 1e18, x, y, j;
    for (y = z.y0 + h / 2; y <= z.y1 - h / 2 + 0.5; y += passo) {
        for (x = z.x0 + w / 2; x <= z.x1 - w / 2 + 0.5; x += passo) {
            var c = gCaixa(x, y, w, h), livre = !gNoTrilho(c, z);
            for (j = 0; livre && j < ocupado.length; j++) if (gToca(c, ocupado[j], folga)) livre = false;
            if (!livre) continue;
            var d = (x - cx) * (x - cx) + (y - cy) * (y - cy);
            if (d < md) { md = d; melhor = c; }
        }
    }
    return melhor;
}

/* O CURSOR é um elemento como os outros: a ponta clica DENTRO do alvo (botão, campo), mas a setinha inteira (da ponta para a
   direita e para baixo) nunca cobre letra. Clica na borda do botão ou depois do texto.
   gCursor(alvo, textos, o) → { ponta: [x, y], c: caixa da setinha, movido, ok }
   o = { w, h (a setinha a partir da ponta), folga, preferido: [x, y], pilula (o alvo é pílula: a ponta fica dentro do arredondado) } */
function gCaixaCursor(p, w, h) { return [p[0] - 3, p[1] - 3, p[0] + w, p[1] + h]; }
function gDentroAlvo(p, c, pilula) {
    var h = gH(c), r = h / 2, cy = gCy(c), m = h * 0.1;
    if (!pilula) return p[0] >= c[0] + m && p[0] <= c[2] - m && p[1] >= c[1] + m && p[1] <= c[3] - m;
    var x = Math.max(c[0] + r, Math.min(c[2] - r, p[0]));
    return (p[0] - x) * (p[0] - x) + (p[1] - cy) * (p[1] - cy) <= (r - m) * (r - m);
}
function gCursor(alvo, textos, o) {
    o = o || {};
    var w = o.w || 48, h = o.h || 70, folga = o.folga != null ? o.folga : 6, i, j, x, y;
    var pref = o.preferido || [alvo[2] - gH(alvo) * 0.45, gCy(alvo) + gH(alvo) * 0.12];
    var livre = function (p) {
        if (!gDentroAlvo(p, alvo, o.pilula !== false)) return false;
        var c = gCaixaCursor(p, w, h);
        for (j = 0; j < textos.length; j++) if (gToca(c, textos[j], folga)) return false;
        return true;
    };
    if (livre(pref)) return { ponta: pref, c: gCaixaCursor(pref, w, h), movido: false, ok: true };
    var passo = Math.max(4, Math.min(gW(alvo), gH(alvo)) / 12), melhor = null, md = 1e18;
    for (y = alvo[1]; y <= alvo[3]; y += passo) {
        for (x = alvo[0]; x <= alvo[2]; x += passo) {
            if (!livre([x, y])) continue;
            var d = (x - pref[0]) * (x - pref[0]) + (y - pref[1]) * (y - pref[1]);
            if (d < md) { md = d; melhor = [x, y]; }
        }
    }
    if (melhor) return { ponta: melhor, c: gCaixaCursor(melhor, w, h), movido: true, ok: true };
    // sem lugar dentro sem cobrir letra: a ponta vai para a borda de baixo do botão, abaixo do texto (a setinha pende para fora)
    var baixo = alvo[3] - gH(alvo) * 0.06;
    for (j = 0; j < textos.length; j++) if (gToca(textos[j], alvo, 0) && textos[j][3] + folga + 3 > baixo) baixo = textos[j][3] + folga + 3;
    var p2 = [alvo[2] - gH(alvo) * 0.5, baixo];
    return { ponta: p2, c: gCaixaCursor(p2, w, h), movido: true, ok: false };
}

/* TEXTO DENTRO DE CARTÃO (faixa, placa, botão): o texto cabe no cartão com folga. Não cabe: o corpo encolhe até o mínimo (kMin);
   ainda não cabe: o cartão cresce em volta do texto (até a caixa `max`, se houver).
   gTextoNoCartao(t, c, o) → { k (escala do texto), cartao (a caixa nova), cresceu, ok, log }   o = { folga, kMin, max } */
function gTextoNoCartao(t, c, o) {
    o = o || {};
    var f = o.folga || 0, kMin = o.kMin == null ? 0.8 : o.kMin, log = [];
    var k = Math.min(1, (gW(c) - 2 * f) / Math.max(1, gW(t)), (gH(c) - 2 * f) / Math.max(1, gH(t)));
    var dentro = t[0] >= c[0] + f - 0.5 && t[2] <= c[2] - f + 0.5 && t[1] >= c[1] + f - 0.5 && t[3] <= c[3] - f + 0.5;
    if (k >= 1 && dentro) return { k: 1, cartao: c, cresceu: false, ok: true, log: log };
    if (k >= 1) k = 1;
    else if (k >= kMin) log.push('texto apertado no cartão: o corpo encolheu para ' + Math.round(k * 100) + '%');
    else k = kMin;
    var w = Math.max(gW(c), gW(t) * k + 2 * f), h = Math.max(gH(c), gH(t) * k + 2 * f), ok = true;
    if (o.max) { if (w > gW(o.max)) { w = gW(o.max); ok = false; } if (h > gH(o.max)) { h = gH(o.max); ok = false; } }
    var nc = gCaixa(gCx(t), gCy(t), w, h), cresceu = w > gW(c) + 0.5 || h > gH(c) + 0.5 || Math.abs(gCy(t) - gCy(c)) > 0.5 || Math.abs(gCx(t) - gCx(c)) > 0.5;
    if (cresceu) log.push('texto vazava do cartão: o cartão foi para ' + Math.round(w) + '×' + Math.round(h) + (ok ? '' : ' (no máximo; o texto ainda encosta)'));
    return { k: k, cartao: nc, cresceu: cresceu, ok: ok, log: log };
}

/*
 * gConsertar(itens, W, H, o) → { itens, log: [texto], trocar: bool, violacoes: [texto] }
 * o = { zona (gZona), folga, so: 'icones' (só move/encolhe/tira ícones e a foto: o kit, com o texto já posto), legenda,
 *       escalaMundo (layout → tela: a regra da foto 55–80% vale pro que se vê) }
 * Cada item ganha: c (a caixa final), k (escala aplicada), dx/dy (deslocamento do centro), removido.
 */
function gConsertar(itens, W, H, o) {
    o = o || {};
    var z = o.zona || gZona(W, H, o), folga = o.folga != null ? o.folga : GUARDA.folga * Math.min(W, H) / 1080;
    var log = [], trocar = false, i, j, a, b, alto = gAlto(W, H), lado = Math.min(W, H), soIcones = o.so == 'icones', em = o.escalaMundo || 1;
    var L = [];
    for (i = 0; i < itens.length; i++) { a = itens[i]; a.c0 = [a.c[0], a.c[1], a.c[2], a.c[3]]; a.removido = false; L.push(a); }
    var pode = function (it) { return !it.fixo && (!soIcones || it.tipo == 'icone' || it.movel); };

    // 1. tamanho por tipo
    for (i = 0; i < L.length; i++) {
        a = L[i];
        if (!pode(a)) continue;
        if (a.tipo == 'texto' && gW(a.c) > W * GUARDA.textoMaxL) { a.c = gEscala(a.c, W * GUARDA.textoMaxL / gW(a.c)); log.push('texto largo: ' + gNome(a) + ' encolheu para ' + Math.round(GUARDA.textoMaxL * 100) + '% da largura'); }
        if (a.tipo == 'icone') {
            var s = Math.max(gW(a.c), gH(a.c)) / (a.infla || 1);
            if (s > lado * GUARDA.iconeMax) { a.c = gEscala(a.c, lado * GUARDA.iconeMax / s); log.push('ícone gigante: ' + gNome(a) + ' de ' + Math.round(s) + ' px para ' + Math.round(lado * GUARDA.iconeMax) + ' px'); }
            else if (s < lado * GUARDA.iconeMin) { a.c = gEscala(a.c, lado * GUARDA.iconeMin / s); log.push('ícone pequeno: ' + gNome(a) + ' cresceu para ' + Math.round(lado * GUARDA.iconeMin) + ' px'); }
        }
        if (a.tipo == 'foto' && alto) {
            var fw = gW(a.c) / (a.infla || 1) * em;   // no MUNDO: a regra é sobre o que o espectador vê
            if (fw < W * GUARDA.fotoMin - 0.5) { a.c = gEscala(a.c, W * GUARDA.fotoMin / fw); log.push('foto pequena: ' + Math.round(fw / W * 100) + '% da largura, cresceu para ' + Math.round(GUARDA.fotoMin * 100) + '%'); }
            else if (fw > W * GUARDA.fotoMax + 0.5) { a.c = gEscala(a.c, W * GUARDA.fotoMax / fw); log.push('foto gigante: ' + Math.round(fw / W * 100) + '% da largura, encolheu para ' + Math.round(GUARDA.fotoMax * 100) + '%'); }
        }
    }
    // 2. zona segura (e o trilho de botões do 9:16)
    for (i = 0; i < L.length; i++) {
        a = L[i];
        if (!pode(a) || a.livre) continue;
        var antes = a.c;
        if (!gCabe(a.c, z)) a.c = gEncaixa(a.c, z);
        if (gNoTrilho(a.c, z) && (a.tipo == 'icone' || a.tipo == 'foto')) {
            var nx = gMove(a.c, z.trilho[0] - folga * 0.5 - a.c[2], 0);
            if (gCabe(nx, z)) a.c = nx; else { var ny = gMove(a.c, 0, z.trilho[1] - a.c[3]); if (gCabe(ny, z)) a.c = ny; }
        }
        if (antes !== a.c) log.push('fora da zona segura: ' + gNome(a) + ' ' + gTxt(antes) + ' → ' + gTxt(a.c));
    }
    // 3. texto × foto/cartão: afasta (a foto sobe ou desce; ou o texto), depois encolhe a foto até o mínimo; senão, troca a peça
    for (i = 0; i < L.length; i++) {
        a = L[i];
        if (a.tipo != 'texto') continue;
        for (j = 0; j < L.length; j++) {
            b = L[j];
            if (b === a || (b.tipo != 'foto' && b.tipo != 'cartao') || !gToca(a.c, b.c, folga)) continue;
            var feito = false, tentativa;
            if (pode(b)) {   // a foto vai para o lado oposto ao do texto
                var cima = gCy(b.c) <= gCy(a.c);
                for (tentativa = 0; tentativa < 12 && !feito; tentativa++) {
                    var nb = cima ? gMove(b.c, 0, a.c[1] - folga - b.c[3]) : gMove(b.c, 0, a.c[3] + folga - b.c[1]);
                    if (gCabe(nb, z)) { log.push('texto encostado na foto: ' + gNome(b) + ' foi para ' + (cima ? 'cima' : 'baixo') + (tentativa ? ' (encolhida ' + Math.round(gW(b.c) / W * 100) + '%)' : '')); b.c = nb; feito = true; break; }
                    var min = b.tipo == 'foto' && alto ? W * GUARDA.fotoMin * (b.infla || 1) / em : gW(b.c0) * 0.6;
                    if (gW(b.c) * 0.94 < min) break;
                    b.c = gEscala(b.c, 0.94);
                }
            }
            if (!feito && pode(a)) {
                var embaixo = gCy(a.c) >= gCy(b.c);
                var na = embaixo ? gMove(a.c, 0, b.c[3] + folga - a.c[1]) : gMove(a.c, 0, b.c[1] - folga - a.c[3]);
                if (gCabe(na, z)) { log.push('texto encostado em ' + gNome(b) + ': o texto foi para ' + (embaixo ? 'baixo' : 'cima')); a.c = na; feito = true; }
            }
            if (!feito) { trocar = true; log.push('não coube: ' + gNome(a) + ' e ' + gNome(b) + ' se sobrepõem sem saída: troca a peça'); }
        }
    }
    // 3b. cursor: nunca em cima de letra; a ponta continua dentro do alvo (o botão), a setinha vai para a borda ou depois do texto
    var textosC = [];
    for (i = 0; i < L.length; i++) if (L[i].tipo == 'texto' && !L[i].removido) textosC.push(L[i].c);
    for (i = 0; i < L.length; i++) {
        a = L[i];
        if (a.tipo != 'cursor' || !a.alvo) continue;
        var bateC = false;
        for (j = 0; j < textosC.length; j++) if (gToca(a.c, textosC[j], folga * 0.25)) bateC = true;
        if (!bateC) continue;
        var rc = gCursor(a.alvo, textosC, { w: gW(a.c) - 3, h: gH(a.c) - 3, folga: folga * 0.25, pilula: a.pilula !== false });
        log.push('cursor sobre o texto: ' + gNome(a) + ' ' + gTxt(a.c) + ' → ' + gTxt(rc.c) + (rc.ok ? '' : ' (na borda do botão)'));
        a.c = rc.c; a.ponta = rc.ponta;
    }
    // 4. ícones: nunca sobre texto, foto/rosto, cartão ou outro ícone; acha um lugar livre, encolhe até o mínimo, ou sai
    var ordem = [];
    for (i = 0; i < L.length; i++) if (L[i].tipo == 'icone') ordem.push(L[i]);
    ordem.sort(function (p, q) { return (q.prio || 0) - (p.prio || 0); });
    var ocupado = [];
    for (i = 0; i < L.length; i++) if (L[i].tipo != 'icone') ocupado.push(L[i].c);
    for (i = 0; i < ordem.length; i++) {
        a = ordem[i];
        var bate = !gCabe(a.c, z) || gNoTrilho(a.c, z), quem = '';
        for (j = 0; !bate && j < ocupado.length; j++) if (gToca(a.c, ocupado[j], folga)) bate = true;
        if (!bate) { ocupado.push(a.c); continue; }
        if (!pode(a)) { log.push('ícone fixo sobreposto: ' + gNome(a)); continue; }
        for (j = 0; j < L.length; j++) if (L[j] !== a && !L[j].removido && L[j].tipo != 'icone' && gToca(a.c, L[j].c, folga)) { quem = L[j].tipo == 'texto' ? 'o texto' : (L[j].tipo == 'foto' ? 'a foto' : gNome(L[j])); break; }
        var w = gW(a.c), h = gH(a.c), achou = null, k = 1, s0 = Math.max(w, h) / (a.infla || 1), minK = Math.min(1, lado * GUARDA.iconeMin / s0);
        while (!achou && k >= minK - 0.001) {
            achou = gLugarLivre(gCx(a.c), gCy(a.c), w * k, h * k, z, ocupado, folga, W);
            if (!achou) k = k > minK + 0.001 ? Math.max(minK, k * 0.85) : minK - 1;
        }
        if (achou) {
            log.push('ícone ' + (quem ? 'sobre ' + quem : 'fora do lugar') + ': ' + gNome(a) + ' ' + gTxt(a.c) + ' → ' + gTxt(achou) + (k < 0.999 ? ' (encolhido ' + Math.round(k * 100) + '%)' : ''));
            a.c = achou; ocupado.push(achou);
        } else { a.removido = true; log.push('ícone sem lugar livre' + (quem ? ' (estava sobre ' + quem + ')' : '') + ': ' + gNome(a) + ' saiu'); }
    }
    // 5. lotação: no máximo N ao mesmo tempo (sai o ícone de menor prioridade)
    var vivos = 0;
    for (i = 0; i < L.length; i++) if (!L[i].removido) vivos++;
    for (i = ordem.length - 1; i >= 0 && vivos > (o.maxSimult || GUARDA.maxSimult); i--) {
        if (ordem[i].removido || !pode(ordem[i])) continue;
        ordem[i].removido = true; vivos--; log.push('elementos demais na cena: ' + gNome(ordem[i]) + ' saiu');
    }
    for (i = 0; i < L.length; i++) {
        a = L[i];
        a.k = gW(a.c) / Math.max(1e-6, gW(a.c0));
        a.dx = gCx(a.c) - gCx(a.c0); a.dy = gCy(a.c) - gCy(a.c0);
    }
    return { itens: L, log: log, trocar: trocar, violacoes: gVerificar(L, W, H, { zona: z, folga: folga * 0.5 }) };
}

/* Só confere (não mexe): o que ainda viola. É a conferência por caixa quando não há olho (Vision) depois do render. */
function gVerificar(itens, W, H, o) {
    o = o || {};
    var z = o.zona || gZona(W, H, o), folga = o.folga != null ? o.folga : 0, v = [], i, j, a, b;
    for (i = 0; i < itens.length; i++) {
        a = itens[i];
        if (a.removido || a.livre) continue;
        if (!gCabe(a.c, { x0: z.x0 - 6, x1: z.x1 + 6, y0: z.y0 - 6, y1: z.y1 + 6 })) v.push('fora da zona segura: ' + gNome(a) + ' ' + gTxt(a.c));
        else if ((a.tipo == 'icone' || a.tipo == 'foto') && gNoTrilho(a.c, z)) v.push('no trilho de botões: ' + gNome(a));
        for (j = i + 1; j < itens.length; j++) {
            b = itens[j];
            if (b.removido || b.livre) continue;
            if (a.tipo == 'texto' && b.tipo == 'texto') continue;   // texto × texto: quem confere é o kit (tempos diferentes)
            if (a.tipo == 'cursor' || b.tipo == 'cursor') {          // o cursor só não pode cobrir letra
                if ((a.tipo == 'texto' || b.tipo == 'texto') && gToca(a.c, b.c, 0)) v.push('cursor sobre o texto: ' + gNome(a.tipo == 'cursor' ? b : a));
                continue;
            }
            if ((a.tipo == 'cartao' && b.tipo == 'cartao')) continue;
            if (gToca(a.c, b.c, folga - 1e-6) && (a.tipo == 'texto' || b.tipo == 'texto' || a.tipo == 'icone' || b.tipo == 'icone')) v.push('sobreposição: ' + gNome(a) + ' × ' + gNome(b));
        }
    }
    return v;
}
