// js/pattern/pattern_ui.js
import { appState } from "../state.js";
import { updateTrackSample } from "./pattern_state.js";
import { playSample, stopPlayback } from "./pattern_audio.js";
import { getTotalSteps } from "../utils.js";
import { sendAction } from '../socket.js';
import { initializeAudioContext } from '../audio.js';
import * as Tone from "https://esm.sh/tone";
// Função principal de renderização para o editor de patterns
export function renderPatternEditor() {
const trackContainer = document.getElementById("track-container");
trackContainer.innerHTML = "";
// 1. LÓGICA DE FILTRAGEM E CONTEXTO
// Decide quais trilhas mostrar baseando-se no ID de Bassline em foco
let tracksToRender = [];
let isFocusedMode = false;
let contextName = "Song Editor"; // Nome da visualização atual
if (appState.pattern.focusedBasslineId) {
isFocusedMode = true;
// 1) acha o container (Caixa/Kick/...) focado
const basslineTrack = appState.pattern.tracks.find(
t => t.id === appState.pattern.focusedBasslineId && t.type === "bassline"
);
// 2) usa o rack real como "pai" (fallback: id do próprio container)
const srcId = basslineTrack?.instrumentSourceId || appState.pattern.focusedBasslineId;
// 3) mostra somente instrumentos pertencentes ao rack
tracksToRender = appState.pattern.tracks.filter(
t => t.type !== "bassline" && (t.parentBasslineId === srcId || t.parentBasslineId == null)
);
// Nome no header
if (basslineTrack) contextName = basslineTrack.name;
} else {
// Modo Padrão: Mostra trilhas da raiz (sem pai) e que NÃO são containers de bassline
tracksToRender = appState.pattern.tracks.filter(
t => t.type !== "bassline" && t.parentBasslineId == null
);
// Fallback: Se a lista ficar vazia (projeto antigo ou mal formatado), mostra tudo que não é container
if (tracksToRender.length === 0) {
tracksToRender = appState.pattern.tracks.filter(t => t.type !== 'bassline');
}
}
// 2. RENDERIZAÇÃO DO CABEÇALHO DE NAVEGAÇÃO
const navHeader = document.createElement("div");
navHeader.className = "editor-nav-header";
// Estilos inline para garantir aparência imediata (mova para CSS depois se preferir)
navHeader.style.padding = "8px 12px";
navHeader.style.backgroundColor = "#2b2b2b";
navHeader.style.marginBottom = "8px";
navHeader.style.display = "flex";
navHeader.style.alignItems = "center";
navHeader.style.justifyContent = "space-between";
navHeader.style.color = "#ddd";
navHeader.style.borderBottom = "1px solid #444";
navHeader.style.fontSize = "0.9rem";
const titleIcon = isFocusedMode ? "fa-th-large" : "fa-music";
const title = document.createElement("span");
title.innerHTML = ` ${contextName}`;
navHeader.appendChild(title);
// não visualizar tracks nem instrumentos sem pattern selecionada
const hasSelection = Number.isInteger(appState.pattern.activePatternIndex);
if (!isFocusedMode && !hasSelection) {
const msg = document.createElement("div");
msg.style.padding = "12px";
msg.style.color = "#bbb";
msg.textContent = "Selecione uma pattern para visualizar/editar.";
trackContainer.appendChild(msg);
updateGlobalPatternSelector();
return;
}
// Botão Voltar (Aparece apenas se estiver dentro de uma Bassline)
if (isFocusedMode) {
const backBtn = document.createElement("button");
backBtn.className = "control-btn";
backBtn.innerHTML = ` Voltar`;
backBtn.style.padding = "4px 10px";
backBtn.style.backgroundColor = "#444";
backBtn.style.border = "1px solid #555";
backBtn.style.borderRadius = "4px";
backBtn.style.color = "white";
backBtn.style.cursor = "pointer";
backBtn.onmouseover = () => backBtn.style.backgroundColor = "#555";
backBtn.onmouseout = () => backBtn.style.backgroundColor = "#444";
backBtn.onclick = () => {
if (window.exitPatternFocus) {
window.exitPatternFocus();
} else {
console.error("Função window.exitPatternFocus não encontrada no escopo global.");
}
};
navHeader.appendChild(backBtn);
}
trackContainer.appendChild(navHeader);
// 3. RENDERIZAÇÃO DAS TRILHAS FILTRADAS
tracksToRender.forEach((trackData) => {
// IMPORTANTE: Precisamos encontrar o índice real no array global
// para que as ações (volume, pan, delete) afetem a trilha certa.
const originalIndex = appState.pattern.tracks.findIndex(t => t.id === trackData.id);
const trackLane = document.createElement("div");
trackLane.className = "track-lane";
trackLane.dataset.trackIndex = originalIndex;
if (trackData.id === appState.pattern.activeTrackId) {
trackLane.classList.add('active-track');
}
trackLane.innerHTML = `
`;
// Eventos de Seleção
trackLane.addEventListener('click', () => {
if (appState.pattern.activeTrackId === trackData.id) return;
// stopPlayback(); // (Opcional: descomente se quiser parar o som ao trocar de track)
appState.pattern.activeTrackId = trackData.id;
document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track'));
trackLane.classList.add('active-track');
updateGlobalPatternSelector();
redrawSequencer();
});
// Drag and Drop de Samples
trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); });
trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over"));
trackLane.addEventListener("drop", (e) => {
e.preventDefault();
trackLane.classList.remove("drag-over");
const filePath = e.dataTransfer.getData("text/plain");
if (filePath) {
sendAction({
type: 'SET_TRACK_SAMPLE',
trackIndex: originalIndex,
filePath: filePath
});
}
});
trackContainer.appendChild(trackLane);
});
updateGlobalPatternSelector();
redrawSequencer();
}
// Renderiza o grid de steps ou piano roll miniatura
export function redrawSequencer() {
const totalGridSteps = getTotalSteps();
document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => {
let sequencerContainer = wrapper.querySelector(".step-sequencer");
if (!sequencerContainer) {
sequencerContainer = document.createElement("div");
sequencerContainer.className = "step-sequencer";
wrapper.appendChild(sequencerContainer);
} else {
sequencerContainer.innerHTML = "";
}
// Busca o dado da trilha usando o índice global
const parentTrackElement = wrapper.closest(".track-lane");
const trackIndex = parseInt(parentTrackElement.dataset.trackIndex, 10);
const trackData = appState.pattern.tracks[trackIndex];
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) return;
const isFocused = !!appState.pattern.focusedBasslineId;
const globalIdx = appState.pattern.activePatternIndex;
// ✅ Song Editor sem pattern selecionada: não mostra composição
if (!isFocused && !Number.isInteger(globalIdx)) {
// opcional: desenha uma grade "apagada" pra manter o layout
for (let i = 0; i < totalGridSteps; i++) {
const stepWrapper = document.createElement("div");
stepWrapper.className = "step-wrapper";
const stepElement = document.createElement("div");
stepElement.className = "step"; // sem .active
stepElement.style.opacity = "0.25";
stepElement.style.pointerEvents = "none";
stepWrapper.appendChild(stepElement);
sequencerContainer.appendChild(stepWrapper);
}
return;
}
// ✅ em foco: se não tiver globalIdx válido, cai no 0 (porque foco sempre edita algo)
const activePatternIndex = isFocused
? (Number.isInteger(globalIdx) ? globalIdx : 0)
: globalIdx;
const activePattern = trackData.patterns[activePatternIndex];
if (!activePattern) return;
// --- DECISÃO: STEP SEQUENCER VS PIANO ROLL ---
const notes = activePattern.notes || [];
let renderMode = 'steps';
if (notes.length > 0) {
const firstKey = notes[0].key;
// Se houver variação de nota ou notas longas, usa visualização de Piano Roll
if (notes.some(n => n.key !== firstKey) || notes.some(n => n.len > 48)) {
renderMode = 'piano_roll';
}
}
if (renderMode === 'piano_roll') {
// --- MODO: MINI PIANO ROLL ---
sequencerContainer.classList.add('mode-piano');
const miniView = document.createElement('div');
miniView.className = 'track-mini-piano-roll';
miniView.title = "Clique duplo para abrir o Piano Roll completo";
miniView.addEventListener('dblclick', (e) => {
e.stopPropagation();
if (window.openPianoRoll) window.openPianoRoll(trackData.id);
});
// 48 ticks = 1/16 step.
const totalTicks = totalGridSteps * 48;
activePattern.notes.forEach(note => {
const noteEl = document.createElement('div');
noteEl.className = 'mini-note';
const leftPercent = Math.min((note.pos / totalTicks) * 100, 100);
const widthPercent = Math.min((note.len / totalTicks) * 100, 100 - leftPercent);
// Mapeia nota MIDI para altura (aprox 4 oitavas visíveis)
const relativeKey = Math.max(0, Math.min(note.key - 36, 48));
const topPercent = 100 - ((relativeKey / 48) * 100);
noteEl.style.left = `${leftPercent}%`;
noteEl.style.width = `${widthPercent}%`;
noteEl.style.top = `${topPercent}%`;
miniView.appendChild(noteEl);
});
sequencerContainer.appendChild(miniView);
} else {
// --- MODO: STEP SEQUENCER (Bateria) ---
sequencerContainer.classList.remove('mode-piano');
const patternSteps = activePattern.steps;
for (let i = 0; i < totalGridSteps; i++) {
const stepWrapper = document.createElement("div");
stepWrapper.className = "step-wrapper";
const stepElement = document.createElement("div");
stepElement.className = "step";
if (patternSteps[i] === true) stepElement.classList.add("active");
// Evento de Toggle
stepElement.addEventListener("click", (e) => {
e.stopPropagation();
initializeAudioContext();
const isActive = !activePattern.steps[i];
sendAction({
type: 'TOGGLE_NOTE',
trackIndex,
patternIndex: activePatternIndex,
stepIndex: i,
isActive
});
// Preview Sonoro
if (isActive) {
if (trackData.type === 'sampler' && trackData.samplePath) {
playSample(trackData.samplePath, trackData.id);
} else if (trackData.type === 'plugin' && trackData.instrument) {
try { trackData.instrument.triggerAttackRelease("C5", "16n", Tone.now()); } catch(err) {}
}
}
});
// Estilização do Grid (Marcadores de Compasso e Beat)
if (Math.floor(i / 4) % 2 === 0) stepElement.classList.add("step-dark");
if (i > 0 && i % 16 === 0) {
const marker = document.createElement("div");
marker.className = "step-marker";
marker.textContent = Math.floor(i / 16) + 1;
stepWrapper.appendChild(marker);
}
stepWrapper.appendChild(stepElement);
sequencerContainer.appendChild(stepWrapper);
}
}
});
}
// Ilumina o step atual durante o playback
export function highlightStep(stepIndex, isActive) {
if (stepIndex < 0) return;
document.querySelectorAll(".track-lane").forEach((track) => {
const stepWrapper = track.querySelector(`.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`);
if (stepWrapper) {
const stepElement = stepWrapper.querySelector(".step");
if (stepElement) stepElement.classList.toggle("playing", isActive);
}
});
}
// Atualiza UI de um step específico (chamado pelo socket)
export function updateStepUI(trackIndex, patternIndex, stepIndex, isActive) {
// Redesenhar tudo é mais seguro para garantir que filtros e views (piano vs steps) estejam corretos
redrawSequencer();
}
// Atualiza o dropdown de patterns na toolbar
export function updateGlobalPatternSelector() {
const sel = document.getElementById("global-pattern-selector");
if (!sel) return;
const isFocused = !!appState.pattern.focusedBasslineId;
// qualquer instrumento serve como referência de nomes/quantidade de patterns
const referenceTrack = (appState.pattern.tracks || []).find(
(t) => t.type !== "bassline" && Array.isArray(t.patterns) && t.patterns.length > 0
);
sel.innerHTML = "";
if (!referenceTrack) {
const opt = document.createElement("option");
opt.textContent = "Sem patterns";
sel.appendChild(opt);
sel.disabled = true;
return;
}
// ✅ fora do foco: permite "limpar seleção"
if (!isFocused) {
const noneOpt = document.createElement("option");
noneOpt.value = "";
noneOpt.textContent = "Selecione uma pattern";
sel.appendChild(noneOpt);
}
referenceTrack.patterns.forEach((p, idx) => {
const opt = document.createElement("option");
opt.value = String(idx);
opt.textContent = p.name || `Pattern ${idx + 1}`;
sel.appendChild(opt);
});
// Define seleção atual
const idx = appState.pattern.activePatternIndex;
if (isFocused) {
sel.value = String(Number.isInteger(idx) ? idx : 0);
} else {
sel.value = Number.isInteger(idx) ? String(idx) : "";
}
sel.disabled = false;
}