param(
  [Parameter(Mandatory=$true)][string]$PlyPath,
  [Parameter(Mandatory=$true)][string]$OutDir,
  [Parameter(Mandatory=$true)][string]$JuceDir,
  [Parameter(Mandatory=$true)][string]$CMakePath
)

$ErrorActionPreference = "Stop"

function Say($Message) {
  Write-Host "[PlyCompiler] $Message"
}

function Progress($Percent, $Message) {
  Write-Host "[PlyProgress] $Percent|$Message"
}

function Escape-Cpp($Value) {
  if ($null -eq $Value) { return "" }
  return ([string]$Value).Replace("\", "\\").Replace('"', '\"')
}

function Safe-Name($Value, $Fallback) {
  $s = [string]$Value
  if ([string]::IsNullOrWhiteSpace($s)) { $s = $Fallback }
  $s = $s -replace '[^a-zA-Z0-9 _.-]', ''
  $s = $s.Trim()
  if ([string]::IsNullOrWhiteSpace($s)) { return $Fallback }
  return $s
}

function Prop($Object, $Name, $Default) {
  if ($null -eq $Object) { return $Default }
  $prop = $Object.PSObject.Properties[$Name]
  if ($null -eq $prop -or $null -eq $prop.Value) { return $Default }
  return $prop.Value
}

function Has-Msvc {
  $roots = @(
    "C:\Program Files\Microsoft Visual Studio\2022\BuildTools",
    "C:\Program Files\Microsoft Visual Studio\2022\Community",
    "C:\Program Files\Microsoft Visual Studio\2022\Professional",
    "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
  )
  foreach ($root in $roots) {
    if (Test-Path (Join-Path $root "VC\Tools\MSVC")) {
      $cl = Get-ChildItem (Join-Path $root "VC\Tools\MSVC") -Recurse -Filter cl.exe -ErrorAction SilentlyContinue |
        Where-Object { $_.FullName -like "*\bin\Hostx64\x64\cl.exe" } |
        Select-Object -First 1
      if ($cl) { return $true }
    }
  }
  return $false
}

if (!(Test-Path $PlyPath)) { throw "No existe el paquete .ply: $PlyPath" }
if (!(Test-Path (Join-Path $JuceDir "CMakeLists.txt"))) { throw "JUCE no valido: $JuceDir" }
if (!(Test-Path $CMakePath)) { throw "CMake no valido: $CMakePath" }
if (!(Has-Msvc)) {
  Progress 12 "Falta Microsoft C++ Build Tools"
  throw "Falta Microsoft C++ Build Tools. En PlyCompiler pulsa Preparar entorno C++."
}

$PlyPath = (Resolve-Path -LiteralPath $PlyPath).Path
$JuceDir = (Resolve-Path -LiteralPath $JuceDir).Path
$CMakePath = (Resolve-Path -LiteralPath $CMakePath).Path

New-Item -ItemType Directory -Force $OutDir | Out-Null

$hashInput = "$PlyPath|$((Get-Item -LiteralPath $PlyPath).LastWriteTimeUtc.Ticks)|$((Get-Item -LiteralPath $PlyPath).Length)"
$sha = [Security.Cryptography.SHA256]::Create()
$hash = [BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($hashInput))).Replace("-", "").Substring(0, 16).ToLowerInvariant()
$cacheRoot = Join-Path $env:LOCALAPPDATA "PlyCompiler\build-cache"
$workRoot = Join-Path $cacheRoot $hash
$extractDir = Join-Path $workRoot "package"
$projectDir = Join-Path $workRoot "project"
$buildDir = Join-Path $workRoot "build"
$sampleDir = Join-Path $projectDir "Resources\Samples"
$sourceDir = Join-Path $projectDir "Source"

if (Test-Path $projectDir) { Remove-Item -LiteralPath $projectDir -Recurse -Force }
if (Test-Path $buildDir) { Remove-Item -LiteralPath $buildDir -Recurse -Force }
New-Item -ItemType Directory -Force $workRoot, $projectDir, $buildDir, $sourceDir | Out-Null
if (Test-Path $extractDir) { Remove-Item -LiteralPath $extractDir -Recurse -Force }
if (Test-Path $sampleDir) { Remove-Item -LiteralPath $sampleDir -Recurse -Force }
New-Item -ItemType Directory -Force $extractDir, $sampleDir | Out-Null

try {
  Progress 5 "Leyendo paquete .ply"
  Say "Extrayendo paquete .ply..."
  $zipCopy = Join-Path $workRoot "package.zip"
  Copy-Item -LiteralPath $PlyPath -Destination $zipCopy -Force
  Expand-Archive -LiteralPath $zipCopy -DestinationPath $extractDir -Force

  $instrumentPath = Join-Path $extractDir "instrument.json"
  if (!(Test-Path $instrumentPath)) { throw "El paquete no contiene instrument.json." }

  Progress 12 "Validando instrumento"
  $inst = Get-Content -LiteralPath $instrumentPath -Raw -Encoding UTF8 | ConvertFrom-Json
  $name = Safe-Name $inst.name "Ply Instrument"
  $productName = Escape-Cpp $name
  $pluginCode = "P" + $hash.Substring(0, 3).ToUpperInvariant()

  Say "Preparando instrumento: $name"

  $zones = @($inst.zones)
  if ($zones.Count -eq 0) { throw "El instrumento no contiene zonas de teclado." }
  $sampleMap = @{}
  $sampleEntries = New-Object System.Collections.Generic.List[string]
  $sampleFiles = New-Object System.Collections.Generic.List[string]
  $zoneLines = New-Object System.Collections.Generic.List[string]

  foreach ($zone in $zones) {
    $lo = [int](Prop $zone "loNote" 21)
    $hi = [int](Prop $zone "hiNote" 108)
    $loVel = [int](Prop $zone "loVel" (Prop $zone "velocityMin" 0))
    $hiVel = [int](Prop $zone "hiVel" (Prop $zone "velocityMax" 127))
    $root = [int](Prop $zone "pitchKey" 60)
    $sampleIndex = -1
    $builtin = 0

    $pkgSample = [string]$zone.packageSamplePath
    if (![string]::IsNullOrWhiteSpace($pkgSample)) {
      $src = Join-Path $extractDir $pkgSample
      if (Test-Path $src) {
        $ext = ([IO.Path]::GetExtension($src)).ToLowerInvariant()
        if ([string]::IsNullOrWhiteSpace($ext)) { $ext = ".wav" }
        $key = (Resolve-Path $src).Path
        if (!$sampleMap.ContainsKey($key)) {
          $idx = $sampleMap.Count
          $dstName = "s$idx$ext"
          $dst = Join-Path $sampleDir $dstName
          Copy-Item -LiteralPath $src -Destination $dst -Force
          $sampleMap[$key] = $idx
          $sampleFiles.Add("Resources/Samples/$dstName") | Out-Null
          $var = ("s{0}_{1}" -f $idx, $ext.TrimStart("."))
          $sampleEntries.Add("  { reinterpret_cast<const char*>(BinaryData::$var), BinaryData::${var}Size },") | Out-Null
        }
        $sampleIndex = [int]$sampleMap[$key]
      }
    }

    if ($sampleIndex -lt 0) {
      $sid = [string]$zone.sampleId
      if ($sid -match 'builtin_(\d+)') { $builtin = [int]$Matches[1] } else { $builtin = 1 }
    }

    $zoneLines.Add("  { $lo, $hi, $loVel, $hiVel, $root, $sampleIndex, $builtin },") | Out-Null
  }

  $missingPackagedSamples = @($zones | Where-Object {
    $sid = [string]$_.sampleId
    $pkg = [string]$_.packageSamplePath
    $sid -and $sid -notmatch '^builtin_' -and [string]::IsNullOrWhiteSpace($pkg)
  })
  if ($missingPackagedSamples.Count -gt 0) {
    throw "El paquete .ply no incluye todos los samples necesarios. Exporta de nuevo desde Ply."
  }

  Progress 22 "Preparando proyecto VST3"
  $volume = 0.8
  if ($inst.effects -and $inst.effects.amp -and $null -ne $inst.effects.amp.volume) {
    $volume = [Math]::Max(0.0, [Math]::Min(1.0, ([double]$inst.effects.amp.volume) / 100.0))
  }
  $volumeLiteral = $volume.ToString("0.0000", [Globalization.CultureInfo]::InvariantCulture)
  $cutoff = 8000.0
  if ($inst.effects -and $inst.effects.filter -and $null -ne $inst.effects.filter.cutoff) { $cutoff = [double]$inst.effects.filter.cutoff }
  $tone = [Math]::Pow([Math]::Max(0.0, [Math]::Min(1.0, ($cutoff - 20.0) / (20000.0 - 20.0))), 0.45)
  $toneLiteral = $tone.ToString("0.0000", [Globalization.CultureInfo]::InvariantCulture)
  $reso = 0.0
  if ($inst.effects -and $inst.effects.filter -and $null -ne $inst.effects.filter.resonance) { $reso = [Math]::Max(0.0, [Math]::Min(0.85, ([double]$inst.effects.filter.resonance) / 100.0 * 0.85)) }
  $resoLiteral = $reso.ToString("0.0000", [Globalization.CultureInfo]::InvariantCulture)
  $attackMs = 10.0
  if ($inst.effects -and $inst.effects.adsr -and $null -ne $inst.effects.adsr.attack) { $attackMs = [Math]::Max(1.0, [double]$inst.effects.adsr.attack) }
  $releaseMs = 500.0
  if ($inst.effects -and $inst.effects.adsr -and $null -ne $inst.effects.adsr.release) { $releaseMs = [Math]::Max(1.0, [double]$inst.effects.adsr.release) }
  $attackStepLiteral = ([Math]::Max(0.00002, [Math]::Min(0.05, 1000.0 / ($attackMs * 44100.0)))).ToString("0.000000", [Globalization.CultureInfo]::InvariantCulture)
  $releaseStepLiteral = ([Math]::Max(0.00002, [Math]::Min(0.05, 1000.0 / ($releaseMs * 44100.0)))).ToString("0.000000", [Globalization.CultureInfo]::InvariantCulture)
  $pan = 0.0
  if ($inst.effects -and $inst.effects.master -and $null -ne $inst.effects.master.pan) { $pan = [double]$inst.effects.master.pan }
  elseif ($inst.effects -and $inst.effects.amp -and $null -ne $inst.effects.amp.pan) { $pan = [double]$inst.effects.amp.pan }
  $panLiteral = ([Math]::Max(-1.0, [Math]::Min(1.0, $pan / 100.0))).ToString("0.0000", [Globalization.CultureInfo]::InvariantCulture)
  $pitch = 0.0
  if ($inst.effects -and $inst.effects.master -and $null -ne $inst.effects.master.pitch) { $pitch = [double]$inst.effects.master.pitch }
  elseif ($inst.effects -and $inst.effects.pitch -and $null -ne $inst.effects.pitch.tune) { $pitch = [double]$inst.effects.pitch.tune }
  $pitchLiteral = ([Math]::Max(-24.0, [Math]::Min(24.0, $pitch))).ToString("0.0000", [Globalization.CultureInfo]::InvariantCulture)
  $width = 1.0
  if ($inst.effects -and $inst.effects.master -and $null -ne $inst.effects.master.width) { $width = ([double]$inst.effects.master.width) / 100.0 }
  $widthLiteral = ([Math]::Max(0.0, [Math]::Min(1.5, $width))).ToString("0.0000", [Globalization.CultureInfo]::InvariantCulture)
  $reverbMix = 0.0
  if ($inst.effects -and $inst.effects.reverb -and $null -ne $inst.effects.reverb.mix) { $reverbMix = ([double]$inst.effects.reverb.mix) / 100.0 }
  $reverbMixLiteral = ([Math]::Max(0.0, [Math]::Min(1.0, $reverbMix))).ToString("0.0000", [Globalization.CultureInfo]::InvariantCulture)

  $binaryBlock = ""
  $binaryLink = ""
  if ($sampleFiles.Count -gt 0) {
    $sources = ($sampleFiles | ForEach-Object { "  $_" }) -join "`n"
    $binaryBlock = "juce_add_binary_data(PlyGenerated_Data SOURCES`n$sources`n)`n"
    $binaryLink = "PlyGenerated_Data"
  }

  $cmake = @"
cmake_minimum_required(VERSION 3.22)
project(PlyGenerated VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_subdirectory("$($JuceDir.Replace('\','/'))" JUCE)

$binaryBlock
juce_add_plugin(PlyGenerated
  COMPANY_NAME "Kaykesoft"
  IS_SYNTH TRUE
  NEEDS_MIDI_INPUT TRUE
  NEEDS_MIDI_OUTPUT FALSE
  IS_MIDI_EFFECT FALSE
  EDITOR_WANTS_KEYBOARD_FOCUS FALSE
  COPY_PLUGIN_AFTER_BUILD FALSE
  PLUGIN_MANUFACTURER_CODE PLYA
  PLUGIN_CODE $pluginCode
  FORMATS VST3
  PRODUCT_NAME "$productName")

juce_generate_juce_header(PlyGenerated)

target_sources(PlyGenerated PRIVATE
  Source/PluginProcessor.cpp)

target_compile_definitions(PlyGenerated PUBLIC
  JUCE_WEB_BROWSER=0
  JUCE_USE_CURL=0
  JUCE_VST3_CAN_REPLACE_VST2=0)

target_link_libraries(PlyGenerated PRIVATE
  $binaryLink
  juce::juce_audio_formats
  juce::juce_audio_processors)
"@

  Set-Content -LiteralPath (Join-Path $projectDir "CMakeLists.txt") -Value $cmake -Encoding UTF8

  $sampleStructEntries = if ($sampleEntries.Count -gt 0) { ($sampleEntries -join "`n") } else { "  { nullptr, 0 }," }
  $zoneStructEntries = $zoneLines -join "`n"
  $instrumentJson = Escape-Cpp (($inst | ConvertTo-Json -Depth 100 -Compress))

  $processor = @"
#include <JuceHeader.h>
#include <algorithm>
#include <cmath>
#include <memory>
#include <vector>

struct EmbeddedSample {
  const char* data;
  int size;
};

struct PlyZone {
  int lo;
  int hi;
  int loVel;
  int hiVel;
  int root;
  int sampleIndex;
  int builtin;
};

static const EmbeddedSample kSamples[] = {
$sampleStructEntries
};

static const PlyZone kZones[] = {
$zoneStructEntries
};

static const char* kInstrumentJson = "$instrumentJson";

class PlyGeneratedAudioProcessor final : public juce::AudioProcessor {
public:
  PlyGeneratedAudioProcessor()
    : AudioProcessor(BusesProperties().withOutput("Output", juce::AudioChannelSet::stereo(), true)) {
    volumeParam = new juce::AudioParameterFloat("volume", "Volume", 0.0f, 1.0f, ${volumeLiteral}f);
    toneParam = new juce::AudioParameterFloat("tone", "Tone", 0.1f, 1.0f, ${toneLiteral}f);
    addParameter(volumeParam);
    addParameter(toneParam);
  }

  const juce::String getName() const override { return "$productName"; }
  bool acceptsMidi() const override { return true; }
  bool producesMidi() const override { return false; }
  bool isMidiEffect() const override { return false; }
  double getTailLengthSeconds() const override { return 0.0; }
  int getNumPrograms() override { return 1; }
  int getCurrentProgram() override { return 0; }
  void setCurrentProgram(int) override {}
  const juce::String getProgramName(int) override { return {}; }
  void changeProgramName(int, const juce::String&) override {}

  void prepareToPlay(double sr, int) override {
    sampleRate = sr > 1.0 ? sr : 44100.0;
    loadSamples();
    for (auto& v : voices) v.active = false;
    reverbBuffer.assign((size_t)juce::jmax(1, (int)(sampleRate * 1.6)), 0.0f);
    reverbIndex = 0;
  }

  void releaseResources() override {}
  bool isBusesLayoutSupported(const BusesLayout& layouts) const override {
    return layouts.getMainOutputChannelSet() == juce::AudioChannelSet::mono()
        || layouts.getMainOutputChannelSet() == juce::AudioChannelSet::stereo();
  }

  void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi) override {
    juce::ScopedNoDenormals noDenormals;
    buffer.clear();

    auto* left = buffer.getWritePointer(0);
    auto* right = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : left;
    const auto n = buffer.getNumSamples();
    const float vol = volumeParam ? volumeParam->get() : 0.8f;
    const float cutoffHz = toneParam ? 20.0f + std::pow(toneParam->get(), 2.22f) * (20000.0f - 20.0f) : 20000.0f;
    const float tone = juce::jlimit(0.02f, 0.98f, 1.0f - std::exp(-2.0f * juce::MathConstants<float>::pi * cutoffHz / (float)sampleRate));
    const float pan = juce::jlimit(-1.0f, 1.0f, panAmount);
    const float leftGain = std::cos((pan + 1.0f) * juce::MathConstants<float>::pi * 0.25f);
    const float rightGain = std::sin((pan + 1.0f) * juce::MathConstants<float>::pi * 0.25f);
    juce::MidiBuffer::Iterator it(midi);
    juce::MidiMessage msg;
    int eventPos = 0;
    bool hasEvent = it.getNextEvent(msg, eventPos);

    for (int i = 0; i < n; ++i) {
      while (hasEvent && eventPos <= i) {
        if (msg.isNoteOn()) noteOn(msg.getNoteNumber(), msg.getFloatVelocity());
        else if (msg.isNoteOff()) noteOff(msg.getNoteNumber());
        hasEvent = it.getNextEvent(msg, eventPos);
      }

      float mixed = 0.0f;
      for (auto& v : voices) {
        if (!v.active) continue;

        if (v.sampleIndex >= 0 && v.sampleIndex < (int)samples.size() && samples[(size_t)v.sampleIndex].getNumSamples() > 0) {
          auto& smp = samples[(size_t)v.sampleIndex];
          const int pos = (int)v.phase;
          if (pos >= smp.getNumSamples()) {
            v.active = false;
            continue;
          }
          const int next = juce::jmin(pos + 1, smp.getNumSamples() - 1);
          const float frac = (float)(v.phase - (double)pos);
          const float s = smp.getSample(0, pos) + (smp.getSample(0, next) - smp.getSample(0, pos)) * frac;
          mixed += s * v.velocity * v.env;
          v.phase += std::pow(2.0, ((double)v.note + (double)pitchOffsetSemis - (double)v.root) / 12.0);
        } else {
          const double hz = 440.0 * std::pow(2.0, (v.note - 69) / 12.0);
          mixed += std::sin(v.phase) * 0.18f * v.velocity * v.env;
          v.phase += juce::MathConstants<double>::twoPi * hz / sampleRate;
          if (v.phase > juce::MathConstants<double>::twoPi) v.phase -= juce::MathConstants<double>::twoPi;
        }
        v.env += v.releasing ? -releaseStep : attackStep;
        v.env = juce::jlimit(0.0f, 1.0f, v.env);
        if (v.releasing && v.env <= 0.0001f) v.active = false;
      }

      filterState += (mixed - filterState) * tone;
      resonanceState += (filterState - resonanceState) * (0.04f + tone * 0.28f);
      float out = filterState + (filterState - resonanceState) * resonanceAmount;
      if (!reverbBuffer.empty() && reverbMix > 0.0001f) {
        const float wet = reverbBuffer[(size_t)reverbIndex];
        reverbBuffer[(size_t)reverbIndex] = out + wet * 0.72f;
        if (++reverbIndex >= (int)reverbBuffer.size()) reverbIndex = 0;
        out = out * (1.0f - reverbMix) + wet * reverbMix;
      }
      const float wide = juce::jlimit(0.0f, 1.5f, widthAmount);
      left[i] = out * vol * (leftGain * (1.0f - wide * 0.18f));
      right[i] = out * vol * (rightGain * (1.0f + wide * 0.18f));
    }
  }

  bool hasEditor() const override { return true; }
  juce::AudioProcessorEditor* createEditor() override;
  void setNamedParam(const juce::String& param, float value, float minValue, float maxValue) {
    const float norm = maxValue > minValue ? juce::jlimit(0.0f, 1.0f, (value - minValue) / (maxValue - minValue)) : value;
    if (param == "master.volume" || param == "amp.volume") {
      if (volumeParam) volumeParam->setValueNotifyingHost(norm);
    } else if (param == "filter.cutoff") {
      if (toneParam) toneParam->setValueNotifyingHost(juce::jlimit(0.0f, 1.0f, std::pow(norm, 0.45f)));
    } else if (param == "filter.resonance") {
      resonanceAmount = juce::jlimit(0.0f, 0.85f, norm * 0.85f);
    } else if (param == "adsr.attack") {
      const float ms = juce::jmax(1.0f, value);
      attackStep = juce::jlimit(0.00002f, 0.05f, 1000.0f / (ms * (float)sampleRate));
    } else if (param == "adsr.release") {
      const float ms = juce::jmax(1.0f, value);
      releaseStep = juce::jlimit(0.00002f, 0.05f, 1000.0f / (ms * (float)sampleRate));
    } else if (param == "master.pan" || param == "amp.pan") {
      panAmount = juce::jlimit(-1.0f, 1.0f, value / 100.0f);
    } else if (param == "master.pitch" || param == "pitch.tune") {
      pitchOffsetSemis = juce::jlimit(-24.0f, 24.0f, value);
    } else if (param == "master.width") {
      widthAmount = juce::jlimit(0.0f, 1.5f, value / 100.0f);
    } else if (param == "reverb.mix") {
      reverbMix = juce::jlimit(0.0f, 1.0f, value / 100.0f);
    }
  }
  float getNamedParam(const juce::String& param, float minValue, float maxValue, float fallback) const {
    if (param == "master.volume" || param == "amp.volume") return volumeParam ? minValue + volumeParam->get() * (maxValue - minValue) : fallback;
    if (param == "filter.cutoff") return toneParam ? minValue + std::pow(toneParam->get(), 1.0f / 0.45f) * (maxValue - minValue) : fallback;
    if (param == "filter.resonance") return minValue + (resonanceAmount / 0.85f) * (maxValue - minValue);
    if (param == "master.pan" || param == "amp.pan") return panAmount * 100.0f;
    if (param == "master.pitch" || param == "pitch.tune") return pitchOffsetSemis;
    if (param == "master.width") return widthAmount * 100.0f;
    if (param == "reverb.mix") return reverbMix * 100.0f;
    return fallback;
  }

  void getStateInformation(juce::MemoryBlock&) override {}
  void setStateInformation(const void*, int) override {}

private:
  struct Voice {
    bool active = false;
    bool releasing = false;
    int note = 60;
    int root = 60;
    int sampleIndex = -1;
    float velocity = 0.0f;
    float env = 0.0f;
    double phase = 0.0;
  };

  void loadSamples() {
    if (loaded) return;
    loaded = true;
    samples.resize(juce::numElementsInArray(kSamples));
    juce::AudioFormatManager fm;
    fm.registerBasicFormats();
    for (int i = 0; i < juce::numElementsInArray(kSamples); ++i) {
      auto stream = std::make_unique<juce::MemoryInputStream>(kSamples[i].data, (size_t)kSamples[i].size, false);
      if (auto reader = std::unique_ptr<juce::AudioFormatReader>(fm.createReaderFor(std::move(stream)))) {
        samples[(size_t)i].setSize(1, (int)reader->lengthInSamples);
        reader->read(&samples[(size_t)i], 0, (int)reader->lengthInSamples, 0, true, false);
      }
    }
  }

  void noteOn(int note, float velocity) {
    const PlyZone* zone = nullptr;
    const int vel = juce::jlimit(1, 127, (int)std::round(velocity * 127.0f));
    for (const auto& z : kZones) {
      if (note >= z.lo && note <= z.hi && vel >= z.loVel && vel <= z.hiVel) {
        zone = &z;
        break;
      }
    }
    if (zone == nullptr) {
      for (const auto& z : kZones) {
        if (note >= z.lo && note <= z.hi) {
          zone = &z;
          break;
        }
      }
    }
    if (zone == nullptr) zone = &kZones[0];

    auto* voice = &voices[0];
    for (auto& v : voices) {
      if (!v.active) {
        voice = &v;
        break;
      }
    }

    voice->active = true;
    voice->releasing = false;
    voice->note = note;
    voice->root = zone->root;
    voice->sampleIndex = zone->sampleIndex;
    voice->velocity = juce::jlimit(0.0f, 1.0f, velocity);
    voice->env = 0.0f;
    voice->phase = 0.0;
  }

  void noteOff(int note) {
    for (auto& v : voices) {
      if (v.active && v.note == note) v.releasing = true;
    }
  }

  double sampleRate = 44100.0;
  bool loaded = false;
  float filterState = 0.0f;
  float resonanceState = 0.0f;
  float resonanceAmount = ${resoLiteral}f;
  float attackStep = ${attackStepLiteral}f;
  float releaseStep = ${releaseStepLiteral}f;
  float panAmount = ${panLiteral}f;
  float pitchOffsetSemis = ${pitchLiteral}f;
  float widthAmount = ${widthLiteral}f;
  float reverbMix = ${reverbMixLiteral}f;
  int reverbIndex = 0;
  std::vector<juce::AudioBuffer<float>> samples;
  std::vector<float> reverbBuffer;
  Voice voices[16];
  juce::AudioParameterFloat* volumeParam = nullptr;
  juce::AudioParameterFloat* toneParam = nullptr;
};

class PlyGeneratedEditor final : public juce::AudioProcessorEditor {
public:
  explicit PlyGeneratedEditor(PlyGeneratedAudioProcessor& p) : AudioProcessorEditor(p), processor(p) {
    auto parsed = juce::JSON::parse(kInstrumentJson);
    if (auto* obj = parsed.getDynamicObject()) {
      if (auto* settings = obj->getProperty("settings").getDynamicObject()) {
        if (auto* size = settings->getProperty("uiSize").getDynamicObject()) {
          uiW = (int)size->getProperty("width");
          uiH = (int)size->getProperty("height");
        }
        bg = settings->getProperty("uiBg").toString();
        if (auto* arr = settings->getProperty("uiControls").getArray()) {
          for (auto& v : *arr) controls.add(v);
        }
      }
      title = obj->getProperty("name").toString();
    }
    if (uiW < 360) uiW = 720;
    if (uiH < 220) uiH = 420;
    setSize(uiW, uiH);
  }

  void paint(juce::Graphics& g) override {
    auto bounds = getLocalBounds().toFloat();
    paintBackground(g, bounds);
    g.setColour(juce::Colours::white.withAlpha(0.82f));
    g.setFont(juce::Font(16.0f, juce::Font::bold));
    if (controls.isEmpty()) g.drawText(title.isNotEmpty() ? title : "Ply Instrument", getLocalBounds().reduced(28), juce::Justification::topLeft);
    for (auto& v : controls) paintControl(g, v);
  }

  void mouseDown(const juce::MouseEvent& e) override {
    activeControl = -1;
    activeStartY = (float)e.y;
    activeStartValue = 0.0f;
    for (int i = controls.size(); --i >= 0;) {
      auto* o = controls.getReference(i).getDynamicObject();
      if (o == nullptr || (bool)o->getProperty("isModuleName")) continue;
      auto param = str(o, "param", "");
      if (param.isEmpty()) continue;
      if (controlBounds(o).contains(e.position)) {
        activeControl = i;
        activeParam = param;
        activeMin = paramMin(param);
        activeMax = paramMax(param);
        activeStartValue = processor.getNamedParam(param, activeMin, activeMax, paramDefault(param));
        break;
      }
    }
  }

  void mouseDrag(const juce::MouseEvent& e) override {
    if (activeControl < 0) return;
    const float range = activeMax - activeMin;
    const float next = juce::jlimit(activeMin, activeMax, activeStartValue + (activeStartY - (float)e.y) * range / 160.0f);
    processor.setNamedParam(activeParam, next, activeMin, activeMax);
    repaint();
  }

private:
  static float num(const juce::DynamicObject* o, const char* k, float fallback) {
    if (o == nullptr) return fallback;
    auto v = o->getProperty(k);
    return v.isVoid() ? fallback : (float)v;
  }
  static juce::String str(const juce::DynamicObject* o, const char* k, const juce::String& fallback = {}) {
    if (o == nullptr) return fallback;
    auto v = o->getProperty(k);
    return v.isVoid() ? fallback : v.toString();
  }
  static juce::Colour colourFrom(const juce::String& value, juce::Colour fallback) {
    if (value.startsWithChar('#')) return juce::Colour::fromString("ff" + value.substring(1));
    if (value.contains("rgb")) return fallback;
    return fallback;
  }
  static float paramMin(const juce::String& p) {
    if (p.contains("volume") || p.contains("mix") || p.contains("depth") || p.contains("width") || p.contains("resonance")) return 0.0f;
    if (p.contains("pan")) return -100.0f;
    if (p.contains("pitch") || p == "pitch.tune") return -24.0f;
    if (p == "filter.cutoff") return 20.0f;
    if (p == "adsr.release") return 1.0f;
    if (p == "adsr.attack") return 1.0f;
    return 0.0f;
  }
  static float paramMax(const juce::String& p) {
    if (p == "filter.cutoff") return 20000.0f;
    if (p == "adsr.release") return 10000.0f;
    if (p == "adsr.attack") return 5000.0f;
    if (p.contains("pan")) return 100.0f;
    if (p.contains("pitch") || p == "pitch.tune") return 24.0f;
    return 100.0f;
  }
  static float paramDefault(const juce::String& p) {
    if (p == "master.volume" || p == "amp.volume") return 80.0f;
    if (p == "filter.cutoff") return 8000.0f;
    if (p == "filter.resonance") return 0.0f;
    if (p == "adsr.attack") return 10.0f;
    if (p == "adsr.release") return 500.0f;
    if (p.contains("pan") || p.contains("pitch") || p == "pitch.tune") return 0.0f;
    if (p == "master.width") return 100.0f;
    if (p == "reverb.mix") return 0.0f;
    return (paramMin(p) + paramMax(p)) * 0.5f;
  }
  static juce::Rectangle<float> controlBounds(const juce::DynamicObject* o) {
    auto type = str(o, "type", "knob");
    auto sizeName = str(o, "size", "md");
    float scale = sizeName == "lg" ? 1.25f : (sizeName == "sm" ? 0.82f : 1.0f);
    auto x = num(o, "x", 20), y = num(o, "y", 20);
    if (type == "slider") return { x - 10, y - 10, 44.0f * scale, 136.0f * scale };
    if (type == "hslider") return { x - 8, y - 8, 116.0f * scale, 34.0f * scale };
    if (type == "toggle" || type == "selector" || type == "button") return { x, y, 94.0f * scale, 40.0f * scale };
    if (type == "xypad") return { x, y, 112.0f * scale, 82.0f * scale };
    return { x, y, 58.0f * scale, 72.0f * scale };
  }
  void paintBackground(juce::Graphics& g, juce::Rectangle<float> b) {
    if (bg.contains("255") || bg.contains("F6") || bg.contains("E9")) {
      g.setGradientFill(juce::ColourGradient(juce::Colour(0xfff5f5f2), 0, 0, juce::Colour(0xffdfe4e5), b.getRight(), b.getBottom(), false));
    } else if (bg.contains("D6C18A") || bg.contains("studio")) {
      g.setGradientFill(juce::ColourGradient(juce::Colour(0xff111315), 0, 0, juce::Colour(0xff272a2a), b.getRight(), b.getBottom(), false));
    } else {
      g.setGradientFill(juce::ColourGradient(juce::Colour(0xff151515), 0, 0, juce::Colour(0xff2b2b31), b.getRight(), b.getBottom(), false));
    }
    g.fillRoundedRectangle(b.reduced(0.5f), 10.0f);
    g.setColour(juce::Colours::white.withAlpha(0.06f));
    for (int x = 0; x < getWidth(); x += 34) g.drawVerticalLine(x, 0.0f, (float)getHeight());
    for (int y = 0; y < getHeight(); y += 34) g.drawHorizontalLine(y, 0.0f, (float)getWidth());
  }
  void paintControl(juce::Graphics& g, const juce::var& v) {
    auto* o = v.getDynamicObject();
    if (o == nullptr) return;
    if ((bool)o->getProperty("isModuleName")) return;
    auto type = str(o, "type", "knob");
    auto label = str(o, "label", "");
    auto x = num(o, "x", 20), y = num(o, "y", 20);
    auto sizeName = str(o, "size", "md");
    float scale = sizeName == "lg" ? 1.25f : (sizeName == "sm" ? 0.82f : 1.0f);
    auto c = colourFrom(str(o, "color", "#007AFF"), juce::Colour(0xff007aff));
    g.setColour(c.withAlpha(0.95f));
    if (type == "label") {
      g.setFont(juce::Font(18.0f * scale, juce::Font::bold));
      g.drawText(label, juce::Rectangle<float>(x, y, 260.0f, 30.0f * scale), juce::Justification::centredLeft);
      return;
    }
    if (type == "slider") {
      juce::Rectangle<float> r(x, y, 24.0f * scale, 112.0f * scale);
      g.setColour(juce::Colours::white.withAlpha(0.18f)); g.fillRoundedRectangle(r, 8.0f);
      g.setColour(c); g.fillRoundedRectangle(r.withTrimmedTop(r.getHeight() * 0.34f), 8.0f);
      g.setColour(juce::Colours::white.withAlpha(0.72f)); g.setFont(juce::Font(10.0f));
      g.drawText(label, r.translated(-32, r.getHeight() + 4).withWidth(88), juce::Justification::centred);
      return;
    }
    if (type == "toggle" || type == "selector" || type == "button") {
      juce::Rectangle<float> r(x, y, 86.0f * scale, 34.0f * scale);
      g.setColour(juce::Colours::white.withAlpha(0.12f)); g.fillRoundedRectangle(r, 7.0f);
      g.setColour(c.withAlpha(0.85f)); g.drawRoundedRectangle(r, 7.0f, 1.5f);
      g.setColour(juce::Colours::white.withAlpha(0.82f)); g.setFont(juce::Font(11.0f * scale, juce::Font::bold));
      g.drawText(label, r, juce::Justification::centred);
      return;
    }
    if (type == "xypad") {
      juce::Rectangle<float> r(x, y, 112.0f * scale, 82.0f * scale);
      g.setColour(juce::Colours::white.withAlpha(0.10f)); g.fillRoundedRectangle(r, 10.0f);
      g.setColour(c.withAlpha(0.7f)); g.drawRoundedRectangle(r, 10.0f, 1.5f);
      g.fillEllipse(r.getCentreX() - 5, r.getCentreY() - 5, 10, 10);
      g.setColour(juce::Colours::white.withAlpha(0.75f)); g.setFont(juce::Font(10.0f));
      g.drawText(label, r.withTrimmedTop(r.getHeight() + 3).withHeight(16), juce::Justification::centred);
      return;
    }
    juce::Rectangle<float> r(x, y, 52.0f * scale, 52.0f * scale);
    g.setColour(juce::Colours::white.withAlpha(0.12f)); g.fillEllipse(r);
    g.setColour(c); g.drawEllipse(r.reduced(2), 3.0f);
    g.drawLine(r.getCentreX(), r.getCentreY(), r.getCentreX() + r.getWidth() * 0.25f, r.getY() + 8, 3.0f);
    g.setColour(juce::Colours::white.withAlpha(0.74f)); g.setFont(juce::Font(10.0f));
    g.drawText(label, r.translated(-18, r.getHeight() + 4).withWidth(88), juce::Justification::centred);
  }

  PlyGeneratedAudioProcessor& processor;
  juce::Array<juce::var> controls;
  juce::String title;
  juce::String bg;
  int uiW = 720;
  int uiH = 420;
  int activeControl = -1;
  float activeStartY = 0.0f;
  float activeStartValue = 0.0f;
  float activeMin = 0.0f;
  float activeMax = 100.0f;
  juce::String activeParam;
};

juce::AudioProcessorEditor* PlyGeneratedAudioProcessor::createEditor() {
  return new PlyGeneratedEditor(*this);
}

juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() {
  return new PlyGeneratedAudioProcessor();
}
"@

  Set-Content -LiteralPath (Join-Path $sourceDir "PluginProcessor.cpp") -Value $processor -Encoding UTF8

  Progress 38 "Configurando motor local"
  Say "Configurando proyecto JUCE..."
  & $CMakePath -S $projectDir -B $buildDir -G "Visual Studio 17 2022" -A x64
  if ($LASTEXITCODE -ne 0) { throw "Fallo CMake configure." }

  Progress 58 "Compilando plugin VST3"
  Say "Compilando VST3 Release..."
  & $CMakePath --build $buildDir --config Release
  if ($LASTEXITCODE -ne 0) { throw "Fallo la compilacion VST3." }

  Progress 88 "Copiando VST3"
  $vst3 = Get-ChildItem -LiteralPath $buildDir -Recurse -Directory -Filter "*.vst3" | Select-Object -First 1
  if (!$vst3) { throw "No se encontro el bundle .vst3 generado." }

  $dest = Join-Path $OutDir $vst3.Name
  if (Test-Path $dest) { Remove-Item -LiteralPath $dest -Recurse -Force }
  Copy-Item -LiteralPath $vst3.FullName -Destination $dest -Recurse -Force

  Say "VST3 listo: $dest"
  Progress 100 "VST3 listo"
} finally {
  if (Test-Path $extractDir) {
    Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue
  }
}
