<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Minimal HTML Chiptune Tracker</title>
<style>
  body { font-family: monospace; background: #111; color: #eee; margin:0; padding:0;}
  #tracker { display: flex; flex-wrap: wrap; max-width: 100vw; overflow-x: auto;}
  .track {
    border: 1px solid #444; margin: 4px; padding: 4px;
    background: #222; min-width: 250px;
    flex: none;
  }
  .track h3 { margin: 0 0 8px 0; font-size: 1.1em;}
  table { border-collapse: collapse; width: 100%; }
  td, th { border: 1px solid #333; padding: 2px 4px; text-align: center; vertical-align: middle; user-select:none;}
  td {
    cursor: pointer;
    min-width: 20px;
  }
  td.selected { background: #3366ff; color: #fff; }
  input, select { width: 100%; box-sizing: border-box; margin-bottom: 6px; background: #111; color: #eee; border: 1px solid #444; }
  button { background: #3366ff; border: none; color: white; padding: 6px 12px; margin-top: 8px; cursor: pointer; }
  button:disabled { background: #444;}
  label { font-size: 0.9em; display: block; margin-top: 6px;}
  #controls {
    background: #222; padding: 8px;
    display: flex; gap: 12px; flex-wrap: wrap;
    align-items: center;
  }
  #shareLink {
    width: 100%; 
    font-size: 0.8em;
  }
</style>
</head>
<body>

<h1 style="text-align:center;">Minimal Chiptune Tracker</h1>

<div id="controls">
  <label>BPM: <input type="number" id="bpm" value="120" min="20" max="300" step="1" /></label>
  <label>Rows per Pattern: <input type="number" id="rowsPerPattern" value="16" min="4" max="64" step="1" /></label>
  <label>Patterns per Track: <input type="number" id="patternsPerTrack" value="4" min="1" max="16" step="1" /></label>
  <label>Tracks: <input type="number" id="numTracks" value="3" min="1" max="8" step="1" /></label>
  <label>Chord Size: <input type="number" id="chordSize" value="1" min="1" max="6" step="1" /></label>
  <button id="btnGenerate">Generate Tracker</button>
  <button id="btnPlay">Play</button>
  <button id="btnStop" disabled>Stop</button>
</div>

<div id="tracker"></div>

<label>Share Song URL:</label>
<input type="text" id="shareLink" readonly />

<script>
// Simple note to frequency converter for one octave C4=middle C 261.63Hz
const NOTES = {
  'C': 261.63,
  'C#': 277.18,
  'D': 293.66,
  'D#': 311.13,
  'E': 329.63,
  'F': 349.23,
  'F#': 369.99,
  'G': 392.00,
  'G#': 415.30,
  'A': 440.00,
  'A#': 466.16,
  'B': 493.88,
};

const noteNames = Object.keys(NOTES);

// Helper: transpose note by semitones
function transposeNote(note, semitones) {
  let baseNote = note.replace(/[#b]/g, '');
  let accidental = note.includes('#') ? 1 : note.includes('b') ? -1 : 0;
  let idx = noteNames.indexOf(baseNote);
  if (idx === -1) return null;
  let newIndex = (idx + semitones + 12) % 12;
  let newNote = noteNames[newIndex];
  if (accidental === 1) newNote += '#';
  else if (accidental === -1) newNote += 'b';
  return newNote;
}

// Map note string to frequency by adjusting octave
function noteToFrequency(note) {
  let match = note.match(/([A-G]#?)(-?\d?)/);
  if (!match) return 0;
  let pitch = match[1];
  let octave = parseInt(match[2]);
  if (isNaN(octave)) octave = 4;
  let baseFreq = NOTES[pitch];
  return baseFreq * Math.pow(2, octave - 4);
}

// Generate all semitones for an octave range - not used yet in this simple version

// --- Tracker state ---
let state = {
  bpm: 120,
  rowsPerPattern: 16,
  patternsPerTrack: 4,
  numTracks: 3,
  chordSize: 1,
  tracks: [], // Each track has patterns, each pattern has rows (chords)
  instruments: [], // For each track: synth settings
  selected: { track: 0, pattern: 0, row: 0, note: 0 },
  playing: false,
  audioCtx: null,
  playInterval: null,
  currentPosition: { track:0, pattern:0, row:0 },
};

// Initialize browser audio context on user gesture
function initAudio() {
  if (!state.audioCtx) {
    state.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  }
}

// Create default instruments (one per track)
function createDefaultInstruments(num) {
  const defaultSynth = {
    oscType: 'square', // 'sine', 'square', 'triangle', 'sawtooth', 'custom'
    volume: 0.3,
    detune: 0, // cents
    adsr: { attack: 0.01, decay: 0.1, sustain: 0.7, release: 0.2},
    filterFreq: 1000,
    filterQ: 1,
    noiseEnabled: false,
  };
  let ins = [];
  for (let i=0; i<num; i++) {
    ins.push({...defaultSynth});
  }
  return ins;
}

// Generate empty pattern data
function generateEmptyPatterns(tracks, patternsPerTrack, rowsPerPattern, chordSize) {
  // Each track: array of patterns
  // Each pattern: array of rows
  // Each row: array of notes or null (length = chordSize)
  let all = [];
  for(let t=0; t<tracks; t++) {
    let patterns = [];
    for(let p=0; p<patternsPerTrack; p++) {
      let rows = [];
      for(let r=0; r<rowsPerPattern; r++) {
        let chord = new Array(chordSize).fill(null); 
        rows.push(chord);
      }
      patterns.push(rows);
    }
    all.push(patterns);
  }
  return all;
}

// Show tracker UI
function renderTracker() {
  const container = document.getElementById('tracker');
  container.innerHTML = '';

  for(let t=0; t < state.numTracks; t++) {
    const trackDiv = document.createElement('div');
    trackDiv.className = 'track';
    trackDiv.dataset.track = t;
    trackDiv.innerHTML = `<h3>Track ${t+1}</h3>`;

    // Instrument controls
    const instr = state.instruments[t];
    const instrDiv = document.createElement('div');
    instrDiv.innerHTML = `
      <label>Oscillator:
        <select data-track="${t}" class="oscType">
          <option${instr.oscType==='sine'? ' selected' : '' }>sine</option>
          <option${instr.oscType==='square'? ' selected' : '' }>square</option>
          <option${instr.oscType==='triangle'? ' selected' : '' }>triangle</option>
          <option${instr.oscType==='sawtooth'? ' selected' : '' }>sawtooth</option>
        </select>
      </label>
      <label>Volume:
        <input type="range" min="0" max="1" step="0.01" value="${instr.volume}" data-track="${t}" class="volume" />
      </label>
      <label>Detune (cents):
        <input type="number" min="-1200" max="1200" step="1" value="${instr.detune}" data-track="${t}" class="detune" />
      </label>
      <label>ADSR (s d s r seconds):
        <input type="text" value="${instr.adsr.attack} ${instr.adsr.decay} ${instr.adsr.sustain} ${instr.adsr.release}" data-track="${t}" class="adsr" />
      </label>
      <label>Filter Freq (Hz):
        <input type="number" min="20" max="20000" value="${instr.filterFreq}" data-track="${t}" class="filterFreq" />
      </label>
      <label>Noise:
        <input type="checkbox" ${instr.noiseEnabled ? 'checked': ''} data-track="${t}" class="noiseEnabled" />
      </label>
    `;
    trackDiv.appendChild(instrDiv);

    // Canvas/editable table for patterns

    for(let p=0; p < state.patternsPerTrack; p++) {
      let patternTitle = document.createElement('h4');
      patternTitle.textContent = `Pattern ${p+1}`;
      trackDiv.appendChild(patternTitle);

      let table = document.createElement('table');
      let thead = document.createElement('thead');
      let trhead = document.createElement('tr');
      trhead.appendChild(document.createElement('th')); // for row num
      for(let n=0; n < state.chordSize; n++) {
        let th = document.createElement('th');
        th.textContent = `Note ${n+1}`;
        trhead.appendChild(th);
      }
      table.appendChild(thead);
      thead.appendChild(trhead);

      let tbody = document.createElement('tbody');
      for(let r=0; r < state.rowsPerPattern; r++) {
        let tr = document.createElement('tr');
        let thRow = document.createElement('th');
        thRow.textContent = (r+1).toString().padStart(2, '0');
        tr.appendChild(thRow);

        for(let n=0; n < state.chordSize; n++) {
          let td = document.createElement('td');
          td.dataset.track = t;
          td.dataset.pattern = p;
          td.dataset.row = r;
          td.dataset.note = n;

          let noteVal = state.tracks[t][p][r][n];
          td.textContent = noteVal ? noteVal : '-';
          // Highlight selected cell
          if (state.selected.track === t && state.selected.pattern === p && state.selected.row === r && state.selected.note === n) {
            td.classList.add('selected');
          }
          tr.appendChild(td);
        }
        tbody.appendChild(tr);
      }
      table.appendChild(tbody);
      trackDiv.appendChild(table);
    }

    container.appendChild(trackDiv);
  }
}

// Update when a cell is clicked
function onCellClick(e) {
  let td = e.target;
  if (!td || td.tagName !== 'TD') return;

  let t = +td.dataset.track;
  let p = +td.dataset.pattern;
  let r = +td.dataset.row;
  let n = +td.dataset.note;
  state.selected = { track: t, pattern: p, row: r, note: n };
  renderTracker();
}

// Keyboard shortcuts for note input
// Use letters A-G possibly with # or b, or numbers 0-9 for note entry

function onKeyDown(e) {
  const {track, pattern, row, note} = state.selected;
  if (track === undefined) return;

  let key = e.key.toUpperCase();

  // Note name entering A-G with optional #, b
  // For simplicity, enter note like C4, D#3, etc.

  if(key === 'BACKSPACE' || key === 'DELETE') {
    state.tracks[track][pattern][row][note] = null;
    renderTracker();
    e.preventDefault();
    return;
  }

  // Accept note names A-G 0-9, #, b for octave, allow simple input
  if(/[A-G]/.test(key)) {
    // Prompt for octave after letter (simple)
    // For simplification - enter note + octave as e.g. "C4" or "D#5"
    // Let's just open a prompt window for entering note here
    let current = state.tracks[track][pattern][row][note] || '';
    let noteInput = prompt('Enter note (e.g. C4, D#3, - to clear):', current);
    if(noteInput === null) return;
    noteInput = noteInput.trim();
    if(noteInput === '-' || noteInput === '') {
      state.tracks[track][pattern][row][note] = null;
    } else {
      // Validate note format roughly
      let valid = /^[A-G](#|b)?-?\d?$/.test(noteInput);
      if (valid) {
        state.tracks[track][pattern][row][note] = noteInput;
      } else {
        alert('Invalid note format');
      }
    }
    renderTracker();
    e.preventDefault();
  }

  // Navigation: arrows to move selection
  if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) {
    let t2=state.selected.track, p2=state.selected.pattern, r2=state.selected.row, n2=state.selected.note;
    switch(e.key) {
      case 'ArrowUp': r2 = (r2 -1 + state.rowsPerPattern) % state.rowsPerPattern; break;
      case 'ArrowDown': r2 = (r2 +1) % state.rowsPerPattern; break;
      case 'ArrowLeft': 
        if(n2 > 0) n2--;
        else {
          if(p2>0) p2--; else p2 = state.patternsPerTrack-1;
          n2 = state.chordSize -1;
        }
        break;
      case 'ArrowRight':
        if(n2 < state.chordSize-1) n2++;
        else {
          if(p2 < state.patternsPerTrack-1) p2++; else p2=0;
          n2=0;
        }
        break;
    }
    state.selected = {track: t2, pattern: p2, row: r2, note: n2};
    renderTracker();
    e.preventDefault();
  }
}

// Play engine
function playSong() {
  initAudio();
  const ctx = state.audioCtx;

  if(state.playing) return;

  let bpm = state.bpm;
  let intervalMs = 60000 / bpm / 4; // 16th note steps (typical for rows)

  let pos = {...state.currentPosition};

  state.playing = true;
  document.getElementById('btnPlay').disabled = true;
  document.getElementById('btnStop').disabled = false;

  state.playInterval = setInterval(() => {
    // Play notes at pos
    playPosition(pos);

    // Advance position
    pos.row++;
    if(pos.row >= state.rowsPerPattern) {
      pos.row = 0;
      pos.pattern++;
      if(pos.pattern >= state.patternsPerTrack) {
        pos.pattern = 0;
        pos.track++;
        if(pos.track >= state.numTracks) {
          pos.track = 0;
        }
      }
    }
    state.currentPosition = {...pos};

  }, intervalMs);
}

// Stop play
function stopSong() {
  if (!state.playing) return;
  state.playing = false;
  clearInterval(state.playInterval);
  document.getElementById('btnPlay').disabled = false;
  document.getElementById('btnStop').disabled = true;
  state.currentPosition = {track:0, pattern:0, row:0};
}

// Play one chord position for all tracks (polyphonic)
function playPosition(pos) {
  const ctx = state.audioCtx;
  for(let t=0; t<state.numTracks; t++) {
    if(t !== pos.track) continue; // We play one track at a time to keep it simple

    let instr = state.instruments[t];
    let pattern = state.tracks[t][pos.pattern];
    if(!pattern) continue;
    let chord = pattern[pos.row];
    if(!chord) continue;

    chord.forEach(noteStr => {
      if (!noteStr) return;
      let freq = noteToFrequency(noteStr);
      if(freq > 0) playNote(freq, instr, ctx);
    });
  }
}

// Play one note with instrument settings
function playNote(freq, instr, ctx) {
  let now = ctx.currentTime;

  // Layer noise if enabled
  if(instr.noiseEnabled) {
    let noiseBuf = ctx.createBuffer(1, ctx.sampleRate * 0.1, ctx.sampleRate);
    let data = noiseBuf.getChannelData(0);
    for(let i=0;i<data.length;i++) {
      data[i] = (Math.random()*2-1)*0.3;
    }
    let noiseSrc = ctx.createBufferSource();
    noiseSrc.buffer = noiseBuf;
    let noiseGain = ctx.createGain();
    noiseGain.gain.setValueAtTime(0.2, now);
    noiseSrc.connect(noiseGain).connect(ctx.destination);
    noiseSrc.start(now);
    noiseSrc.stop(now + 0.1);
  }

  // Oscillator + ADSR envelope + filter
  let osc = ctx.createOscillator();
  osc.type = instr.oscType;
  osc.frequency.setValueAtTime(freq, now);
  osc.detune.setValueAtTime(instr.detune, now);

  let gainNode = ctx.createGain();
  gainNode.gain.setValueAtTime(0, now);

  let filter = ctx.createBiquadFilter();
  filter.type = 'lowpass';
  filter.frequency.setValueAtTime(instr.filterFreq, now);
  filter.Q.setValueAtTime(instr.filterQ, now);

  osc.connect(filter).connect(gainNode).connect(ctx.destination);

  // ADSR
  let attack = instr.adsr.attack;
  let decay = instr.adsr.decay;
  let sustain = instr.adsr.sustain;
  let release = instr.adsr.release;

  gainNode.gain.cancelScheduledValues(now);
  gainNode.gain.setValueAtTime(0, now);
  gainNode.gain.linearRampToValueAtTime(instr.volume, now+attack);
  gainNode.gain.linearRampToValueAtTime(instr.volume*sustain, now+attack+decay);

  osc.start(now);

  // Stop osc after envelope
  gainNode.gain.setTargetAtTime(0, now+attack+decay+0.1, release);

  osc.stop(now + attack + decay + release + 0.2);
}

// Serialize current song into JSON string then URL encode for sharing
function serializeSong() {
  let songData = {
    bpm: state.bpm,
    rowsPerPattern: state.rowsPerPattern,
    patternsPerTrack: state.patternsPerTrack,
    numTracks: state.numTracks,
    chordSize: state.chordSize,
    tracks: state.tracks,
    instruments: state.instruments,
  };
  let json = JSON.stringify(songData);
  return encodeURIComponent(json);
}

// Load song data from URL hash if present
function loadSongFromHash() {
  if(location.hash.length > 2) {
    try {
      let json = decodeURIComponent(location.hash.slice(2));
      let songData = JSON.parse(json);
      state.bpm = songData.bpm || state.bpm;
      state.rowsPerPattern = songData.rowsPerPattern || state.rowsPerPattern;
      state.patternsPerTrack = songData.patternsPerTrack || state.patternsPerTrack;
      state.numTracks = songData.numTracks || state.numTracks;
      state.chordSize = songData.chordSize || state.chordSize;
      state.tracks = songData.tracks || state.tracks;
      state.instruments = songData.instruments || state.instruments;

      // Set controls to matches
      document.getElementById('bpm').value = state.bpm;
      document.getElementById('rowsPerPattern').value = state.rowsPerPattern;
      document.getElementById('patternsPerTrack').value = state.patternsPerTrack;
      document.getElementById('numTracks').value = state.numTracks;
      document.getElementById('chordSize').value = state.chordSize;

      renderTracker();
      updateShareLink();
    } catch(e) {
      console.warn('Bad song data in URL hash');
    }
  }
}

// Update share box with current song url
function updateShareLink() {
  const baseUrl = window.location.origin + window.location.pathname;
  let data = serializeSong();
  let shareInput = document.getElementById('shareLink');
  shareInput.value = `${baseUrl}#/` + data;
  // Update location hash but do not reload
  history.replaceState(null, '', '#/' + data);
}

// Generate tracker with updated settings
function generateTrackerFromSettings() {
  state.bpm = +document.getElementById('bpm').value;
  state.rowsPerPattern = +document.getElementById('rowsPerPattern').value;
  state.patternsPerTrack = +document.getElementById('patternsPerTrack').value;
  state.numTracks = +document.getElementById('numTracks').value;
  state.chordSize = +document.getElementById('chordSize').value;

  // Initialize instruments
  state.instruments = createDefaultInstruments(state.numTracks);

  // Initialize empty patterns
  state.tracks = generateEmptyPatterns(state.numTracks, state.patternsPerTrack, state.rowsPerPattern, state.chordSize);

  state.selected = {track:0,pattern:0,row:0,note:0};
  renderTracker();
  updateShareLink();
}

// Attach event listeners to instrument controls dynamically
function attachInstrumentControls() {
  document.getElementById('tracker').addEventListener('change', (e) => {
    let tgt = e.target;
    let t = +tgt.dataset.track;
    if(t < 0 || t >= state.numTracks) return;

    let instr = state.instruments[t];
    if(tgt.classList.contains('oscType')) {
      instr.oscType = tgt.value;
    } else if(tgt.classList.contains('volume')) {
      instr.volume = parseFloat(tgt.value);
    } else if(tgt.classList.contains('detune')) {
      instr.detune = parseInt(tgt.value);
    } else if(tgt.classList.contains('adsr')) {
      // parse space separated attack decay sustain release
      let v = tgt.value.split(' ').map(x=>parseFloat(x));
      if(v.length ===4 && v.every(n=>!isNaN(n))) {
        instr.adsr.attack = v[0];
        instr.adsr.decay = v[1];
        instr.adsr.sustain = v[2];
        instr.adsr.release = v[3];
      }
      tgt.value = `${instr.adsr.attack} ${instr.adsr.decay} ${instr.adsr.sustain} ${instr.adsr.release}`;
    } else if(tgt.classList.contains('filterFreq')) {
      instr.filterFreq = parseInt(tgt.value);
    } else if(tgt.classList.contains('noiseEnabled')) {
      instr.noiseEnabled = tgt.checked;
    }
    updateShareLink();
  });
}

document.getElementById('btnGenerate').addEventListener('click', () => {
  if(state.playing) stopSong();
  generateTrackerFromSettings();
});

document.getElementById('btnPlay').addEventListener('click', () => {
  playSong();
});
document.getElementById('btnStop').addEventListener('click', () => {
  stopSong();
});

document.getElementById('tracker').addEventListener('click', onCellClick);
window.addEventListener('keydown', onKeyDown);

attachInstrumentControls();

// On load
loadSongFromHash();
if(state.tracks.length === 0) {
  generateTrackerFromSettings();
}

function repeat() {