import type { Peca } from "./documento";

/*
 * A ordem das camadas com grupos (o painel Camadas e o frente/trás).
 *
 * `pecas` vai de trás para a frente (a última fica por cima). Um grupo é um
 * bloco: agrupar junta as peças dele uma em cima da outra, no lugar da de cima;
 * frente, trás, subir e descer levam o grupo inteiro. Escolhida sozinha (dois
 * cliques no grupo), a peça anda só entre as do grupo dela.
 *
 * Arte antiga pode ter grupo com peças espalhadas: o painel mostra o grupo no
 * lugar da peça de cima e a primeira mexida junta o bloco (nada muda sozinho
 * ao abrir, para o desenho não trocar de ordem sem a pessoa pedir).
 */

export type UnidadeDaCamada = { tipo: "peca"; peca: Peca } | { tipo: "grupo"; grupo: string; pecas: Peca[] };

/** As camadas de cima para baixo: peça solta ou grupo (com as peças dele, de cima para baixo). */
export function unidadesDasCamadas(pecas: Peca[]): UnidadeDaCamada[] {
  const porGrupo = new Map<string, Peca[]>();
  for (const p of pecas) if (p.grupo) porGrupo.set(p.grupo, [...(porGrupo.get(p.grupo) || []), p]);
  const vistos = new Set<string>();
  const lista: UnidadeDaCamada[] = [];
  for (let i = pecas.length - 1; i >= 0; i--) {
    const p = pecas[i];
    const membros = p.grupo ? porGrupo.get(p.grupo)! : null;
    /* Grupo de uma peça só (as outras apagadas) aparece como peça. */
    if (!membros || membros.length < 2) lista.push({ tipo: "peca", peca: p });
    else if (!vistos.has(p.grupo!)) {
      vistos.add(p.grupo!);
      lista.push({ tipo: "grupo", grupo: p.grupo!, pecas: [...membros].reverse() });
    }
  }
  return lista;
}

/* As unidades de volta para a ordem de trás para a frente. */
function emOrdem(unidades: UnidadeDaCamada[]): Peca[] {
  return [...unidades].reverse().flatMap((u) => (u.tipo === "peca" ? [u.peca] : [...u.pecas].reverse()));
}

/** Junta as peças do grupo num bloco, no lugar da de cima (ao agrupar). */
export function juntarGrupo(pecas: Peca[], grupo: string): Peca[] {
  const membros = pecas.filter((p) => p.grupo === grupo);
  if (membros.length < 2) return pecas;
  const topo = pecas.findLastIndex((p) => p.grupo === grupo);
  const antes = pecas.slice(0, topo + 1).filter((p) => p.grupo !== grupo);
  const depois = pecas.slice(topo + 1);
  return [...antes, ...membros, ...depois];
}

type Para = "frente" | "tras" | "subir" | "descer";

/* Move um bloco de unidades (as que contêm os ids) para a frente, para trás, ou uma unidade acima/abaixo. */
function moverUnidades(unidades: UnidadeDaCamada[], ids: Set<string>, para: Para): UnidadeDaCamada[] {
  const tem = (u: UnidadeDaCamada) => (u.tipo === "peca" ? ids.has(u.peca.id) : u.pecas.some((p) => ids.has(p.id)));
  const escolhidas = unidades.filter(tem);
  if (!escolhidas.length) return unidades;
  const resto = unidades.filter((u) => !tem(u));
  /* A lista vai de cima para baixo: "frente" é o começo. */
  if (para === "frente") return [...escolhidas, ...resto];
  if (para === "tras") return [...resto, ...escolhidas];
  const primeira = unidades.findIndex(tem);
  const ultima = unidades.findLastIndex(tem);
  /* Onde o bloco entra no resto: uma unidade de fora acima (subir) ou abaixo (descer). */
  const foraAcima = unidades.slice(0, primeira).length;
  const foraAte = unidades.slice(0, ultima + 1).filter((u) => !tem(u)).length;
  const lugar = para === "subir" ? Math.max(0, foraAcima - 1) : Math.min(resto.length, foraAte + 1);
  return [...resto.slice(0, lugar), ...escolhidas, ...resto.slice(lugar)];
}

/**
 * Frente, trás, subir ou descer as escolhidas. Escolha dentro de um grupo só
 * (não o grupo inteiro): anda entre as peças do grupo. O resto: o bloco das
 * escolhidas (cada grupo tocado vai inteiro) anda entre as camadas.
 */
export function ordenarEscolhidas(pecas: Peca[], ids: string[], para: Para): Peca[] {
  const conjunto = new Set(ids);
  const escolhidas = pecas.filter((p) => conjunto.has(p.id));
  if (!escolhidas.length) return pecas;
  const grupo = escolhidas[0].grupo;
  const membros = grupo ? pecas.filter((p) => p.grupo === grupo) : [];
  const dentroDoGrupo = Boolean(grupo) && membros.length >= 2 && escolhidas.every((p) => p.grupo === grupo) && escolhidas.length < membros.length;
  if (dentroDoGrupo) {
    const juntas = juntarGrupo(pecas, grupo!);
    const inicio = juntas.findIndex((p) => p.grupo === grupo);
    const bloco = juntas.slice(inicio, inicio + membros.length).reverse().map((peca) => ({ tipo: "peca" as const, peca }));
    const novoBloco = emOrdem(moverUnidades(bloco, conjunto, para));
    return [...juntas.slice(0, inicio), ...novoBloco, ...juntas.slice(inicio + membros.length)];
  }
  return emOrdem(moverUnidades(unidadesDasCamadas(pecas), conjunto, para));
}

/**
 * Camada(s) arrastada(s) no painel e solta(s) junto de outra: acima ou abaixo
 * dela. `alvo` é uma peça (solta ou dentro de um grupo aberto) ou um grupo
 * (fechado: o bloco inteiro). Solta entre as peças de um grupo, a peça entra
 * nele; tirada do grupo dela, sai (fica solta).
 */
export function soltarNaCamada(pecas: Peca[], ids: string[], alvo: { peca?: string; grupo?: string }, lugar: "acima" | "abaixo"): Peca[] {
  const conjunto = new Set(ids);
  const arrastadas = pecas.filter((p) => conjunto.has(p.id));
  if (!arrastadas.length) return pecas;
  const alvoPeca = alvo.peca ? pecas.find((p) => p.id === alvo.peca) : null;
  if (alvoPeca && conjunto.has(alvoPeca.id)) return pecas;
  if (alvo.grupo && arrastadas.every((p) => p.grupo === alvo.grupo)) return pecas;
  /* Entrar no grupo: soltou numa peça de grupo (que aparece aberto). Arrastou o grupo inteiro: continua grupo dele. */
  const grupoDasArrastadas = arrastadas[0].grupo && arrastadas.every((p) => p.grupo === arrastadas[0].grupo) && pecas.filter((p) => p.grupo === arrastadas[0].grupo).length === arrastadas.length ? arrastadas[0].grupo : undefined;
  /* Grupo inteiro solto numa peça de outro grupo: vale o bloco do outro (um grupo não entra no meio de outro). */
  if (grupoDasArrastadas && alvoPeca?.grupo && alvoPeca.grupo !== grupoDasArrastadas) return soltarNaCamada(pecas, ids, { grupo: alvoPeca.grupo }, lugar);
  const grupoDoAlvo = alvoPeca?.grupo && !grupoDasArrastadas ? alvoPeca.grupo : undefined;
  const movidas = arrastadas.map((p) => (grupoDasArrastadas ? p : p.grupo === grupoDoAlvo ? p : ({ ...p, grupo: grupoDoAlvo } as Peca)));
  const resto = pecas.filter((p) => !conjunto.has(p.id));
  /* O índice (de trás para a frente) onde a camada de referência termina ou começa. */
  let indice: number;
  if (alvo.grupo && !alvoPeca) {
    const primeiro = resto.findIndex((p) => p.grupo === alvo.grupo);
    const ultimo = resto.findLastIndex((p) => p.grupo === alvo.grupo);
    if (primeiro < 0) return pecas;
    indice = lugar === "acima" ? ultimo + 1 : primeiro;
  } else {
    const i = resto.findIndex((p) => p.id === alvo.peca);
    if (i < 0) return pecas;
    /* Peça de grupo fechado (sem entrar nele): vale a ponta do bloco. */
    indice = lugar === "acima" ? i + 1 : i;
  }
  const juntas = [...resto.slice(0, indice), ...movidas, ...resto.slice(indice)];
  return grupoDoAlvo ? juntarGrupo(juntas, grupoDoAlvo) : juntas;
}
