/**
 * This file is part of EWItool.

    EWItool is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    EWItool is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with EWItool.  If not, see <http://www.gnu.org/licenses/>.
 */

package ewitool;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.geometry.HPos;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.Slider;
import javafx.scene.control.Tab;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.RowConstraints;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;

public class PatchEditorTab extends Tab {

  enum Osc { OSC1, OSC2 }
  enum Filter { OSC_PRI, OSC_SEC, NOISE_PRI, NOISE_SEC }
  public ComboBox<String> patchesCombo;
  
  UiOscGrid osc1Grid, osc2Grid;
  UiFormantGrid formantGrid;
  UiKeyTriggerGrid keyTriggerGrid;
  UiNoiseGrid noiseGrid;
  UiFilterGrid oscPriFilterGrid, oscSecFilterGrid, noisePriFilterGrid, noiseSecFilterGrid;
  UiChorusGrid chorusGrid;
  UiDelayGrid delayGrid;
  UiReverbGrid reverbGrid;
  UiBiteGrid biteGrid;
  UiPitchBendGrid pitchBendGrid;
  UiAntiAliasGrid antiAliasGrid;
  UiLevelsGrid levelsGrid;
  
  private volatile EWI4000sPatch editPatch, uneditedPatch;
  MidiHandler midiHandler;
  SharedData sharedData;
  ScratchPad scratchPad;
 
  PatchEditorTab(SharedData pSharedData, ScratchPad pScratchPad, MidiHandler pMidiHandler) {
    setText( "Patch Editor" );
    setClosable( false );

    sharedData = pSharedData;
    scratchPad = pScratchPad;
    midiHandler = pMidiHandler;
    
    editPatch = new EWI4000sPatch();

    GridPane peGrid = new GridPane();
    peGrid.setId( "editor-outer-grid" );
    ColumnConstraints ccsGrowable = new ColumnConstraints( 40.0, 90.0, Double.MAX_VALUE );
    RowConstraints rcsFixed = new RowConstraints(), rcsGrowable = new RowConstraints();
    ccsGrowable.setHgrow( Priority.ALWAYS );
    rcsFixed.setVgrow( Priority.NEVER );
    rcsGrowable.setVgrow( Priority.ALWAYS );
    for (int col = 0; col < 12; col++) peGrid.getColumnConstraints().add( ccsGrowable );
    peGrid.getRowConstraints().addAll( rcsFixed, rcsGrowable, rcsGrowable, rcsGrowable, rcsGrowable );

    patchesCombo = new ComboBox<>();

    HBox headerBox = new HBox();
    headerBox.setId( "editor-header-box" );
    Region lSpaceRegion = new Region(), rSpaceRegion = new Region();
    HBox.setHgrow( lSpaceRegion, Priority.ALWAYS );
    HBox.setHgrow( rSpaceRegion, Priority.ALWAYS );
    headerBox.getChildren().addAll( lSpaceRegion, patchesCombo, rSpaceRegion );

    osc1Grid = new UiOscGrid( editPatch, midiHandler, Osc.OSC1 );
    osc2Grid = new UiOscGrid( editPatch, midiHandler, Osc.OSC2 );

    formantGrid = new UiFormantGrid( editPatch, midiHandler );
    keyTriggerGrid = new UiKeyTriggerGrid( editPatch, midiHandler );
    VBox subVbox = new VBox();
    VBox.setVgrow( formantGrid, Priority.ALWAYS );
    VBox.setVgrow( keyTriggerGrid, Priority.ALWAYS );
    subVbox.getChildren().addAll( formantGrid, keyTriggerGrid );
    oscPriFilterGrid = new UiFilterGrid( editPatch, midiHandler, Filter.OSC_PRI );
    oscSecFilterGrid = new UiFilterGrid( editPatch, midiHandler, Filter.OSC_SEC );

    noiseGrid = new UiNoiseGrid( editPatch, midiHandler );
    noisePriFilterGrid = new UiFilterGrid( editPatch, midiHandler, Filter.NOISE_PRI );
    noiseSecFilterGrid = new UiFilterGrid( editPatch, midiHandler, Filter.NOISE_SEC );

    chorusGrid = new UiChorusGrid( editPatch, midiHandler);
    delayGrid  = new UiDelayGrid( editPatch, midiHandler );
    reverbGrid = new UiReverbGrid( editPatch, midiHandler );
    biteGrid = new UiBiteGrid( editPatch, midiHandler);
    pitchBendGrid = new UiPitchBendGrid( editPatch, midiHandler);
    antiAliasGrid = new UiAntiAliasGrid( editPatch, midiHandler);
    levelsGrid = new UiLevelsGrid( editPatch, midiHandler );

    GridPane.setColumnSpan( headerBox, 12 );
    peGrid.add( headerBox, 0, 0 );

    GridPane.setColumnSpan( osc1Grid, 6 );
    peGrid.add( osc1Grid, 0, 1 );
    GridPane.setColumnSpan( osc2Grid, 6 );
    peGrid.add( osc2Grid, 6, 1 );

    GridPane.setColumnSpan( subVbox, 2 );
    peGrid.add( subVbox, 0, 2 );
    GridPane.setColumnSpan( oscPriFilterGrid, 5 );
    peGrid.add( oscPriFilterGrid, 2, 2 );
    GridPane.setColumnSpan( oscSecFilterGrid, 5 );
    peGrid.add( oscSecFilterGrid, 7, 2 );

    GridPane.setColumnSpan( noiseGrid, 2 );
    peGrid.add( noiseGrid, 0, 3 );
    GridPane.setColumnSpan( noisePriFilterGrid, 5 );
    peGrid.add( noisePriFilterGrid, 2, 3 );
    GridPane.setColumnSpan( noiseSecFilterGrid, 5 );
    peGrid.add( noiseSecFilterGrid, 7, 3 );

    GridPane.setColumnSpan( chorusGrid, 4 );
    peGrid.add( chorusGrid, 0, 4 );
    GridPane.setColumnSpan( delayGrid, 2 );
    peGrid.add( delayGrid, 4, 4 );
    GridPane.setColumnSpan( reverbGrid, 2 );
    peGrid.add( reverbGrid, 6, 4 );
    peGrid.add( biteGrid, 8, 4 );
    peGrid.add( pitchBendGrid, 9, 4 );
    peGrid.add( antiAliasGrid, 10, 4 );
    peGrid.add( levelsGrid, 11, 4 );

    setContent( peGrid );

    // what to do when the combo is changed...
    patchesCombo.setOnAction( (ae) -> {
      if (!patchesCombo.getSelectionModel().isEmpty()) {
        midiHandler.ignoreEvents = true;
        editPatch.patchBlob = sharedData.ewiPatchList[ patchesCombo.getSelectionModel().getSelectedIndex() ].patchBlob;
        editPatch.decodeBlob();
        uneditedPatch = editPatch;
        Debugger.log( "DEBUG - Patch editor selection changed" );
        setAllControls();
        midiHandler.ignoreEvents = false;
        midiHandler.sendPatch( editPatch, EWI4000sPatch.EWI_EDIT ); 
      }
    });
  }
  
  private void setAllControls() {
    osc1Grid.setControls( editPatch, Osc.OSC1 );
    osc2Grid.setControls( editPatch, Osc.OSC2 );
    formantGrid.setControls( editPatch );
    keyTriggerGrid.setControls( editPatch );
    oscPriFilterGrid.setControls( editPatch, Filter.OSC_PRI );
    oscSecFilterGrid.setControls( editPatch, Filter.OSC_SEC );
    noiseGrid.setControls( editPatch );
    noisePriFilterGrid.setControls( editPatch, Filter.NOISE_PRI );
    noiseSecFilterGrid.setControls( editPatch, Filter.NOISE_SEC );
    chorusGrid.setControls( editPatch );
    delayGrid.setControls( editPatch );
    reverbGrid.setControls( editPatch );
    biteGrid.setControls( editPatch );
    pitchBendGrid.setControls( editPatch );
    antiAliasGrid.setControls( editPatch );
    levelsGrid.setControls( editPatch );

  }

  public void populateCombo( SharedData sharedData ) {
    patchesCombo.getItems().clear();
    for (int p = 0; p < EWI4000sPatch.EWI_NUM_PATCHES; p++) {
      patchesCombo.getItems().add( p + " - " + sharedData.ewiPatchList[p].getName() );
    }
  }
  
  // actions invoked by the main menu...
  
  public void store() {
    editPatch.encodeBlob();
    midiHandler.sendPatch( editPatch, EWI4000sPatch.EWI_SAVE );
    sharedData.ewiPatchList[editPatch.internalPatchNum] = editPatch;
    sharedData.setStatusMessage( "Patch #" + (editPatch.internalPatchNum + 1 ) + " stored in EWI" );
  }
  
  public void revert() {
    editPatch = uneditedPatch;
    Event.fireEvent( patchesCombo, new ActionEvent() );
  }
  
  public void copyToScratchPad() {
    scratchPad.addPatch( editPatch );
    sharedData.setStatusMessage( editPatch.getName() + " added to Scratchpad" );
  }
  
  public void makeDry() {
    if (chorusGrid.enableCheck.isSelected()) chorusGrid.enableCheck.fire();
    chorusGrid.drySlider.setValueChanging( true );
    chorusGrid.drySlider.setValue( 127 );
    delayGrid.drySlider.setValueChanging( true );
    delayGrid.drySlider.setValue( 127 );
    delayGrid.volSlider.setValueChanging( true );
    delayGrid.volSlider.setValue( 0 );
    reverbGrid.drySlider.setValueChanging( true );
    reverbGrid.drySlider.setValue( 127 );
    reverbGrid.volSlider.setValueChanging( true );
    reverbGrid.volSlider.setValue( 0 );
  }
  
  public void makeMaxVol() {
    
    // find highest level
    int orig_max = editPatch.osc1.level;
    if (editPatch.osc2.level > orig_max) orig_max = editPatch.osc2.level;
    if (editPatch.ampLevel > orig_max) orig_max = editPatch.ampLevel;
    if (editPatch.octaveLevel > orig_max) orig_max = editPatch.octaveLevel;

    double factor = 127.0 / orig_max;
    
    osc1Grid.volSlider.setValueChanging( true );
    osc1Grid.volSlider.setValue( editPatch.osc1.level * factor );
    osc2Grid.volSlider.setValueChanging( true );
    osc2Grid.volSlider.setValue( editPatch.osc2.level * factor );
    levelsGrid.masterSlider.setValueChanging( true );
    levelsGrid.masterSlider.setValue( editPatch.ampLevel * factor );
    levelsGrid.octaveSlider.setValueChanging( true );
    levelsGrid.octaveSlider.setValue( editPatch.octaveLevel * factor );
  }
  
  public void makeNoNoise() {
    noiseGrid.timeSlider.setValueChanging( true );
    noiseGrid.timeSlider.setValue( 0 );
    noiseGrid.breathSlider.setValueChanging( true );
    noiseGrid.breathSlider.setValue( 0 );
    noiseGrid.volSlider.setValueChanging( true );
    noiseGrid.volSlider.setValue( 0 );
    noisePriFilterGrid.typeChoice.getSelectionModel().select( 4 );  // event does get fired
    noiseSecFilterGrid.typeChoice.getSelectionModel().select( 4 );
  } 
  
  public void randomiseBy10pct() {
    
    /* There is a risk of flooding the EWI if we change everything via CCs,
     * so we change the editPatch object and reset the UI.
     */
    midiHandler.ignoreEvents = true;
    uneditedPatch = editPatch;
    
    editPatch.osc1.fine = randNear( 0, 127, editPatch.osc1.fine );
    editPatch.osc1.beat = randNear( 0, 127, editPatch.osc1.beat );
    editPatch.osc1.sawtooth = randNear( 0, 127, editPatch.osc1.sawtooth );
    editPatch.osc1.triangle = randNear( 0, 127, editPatch.osc1.triangle );
    editPatch.osc1.square = randNear( 0, 127, editPatch.osc1.square );
    editPatch.osc1.pulseWidth = randNear( 0, 127, editPatch.osc1.pulseWidth );
    editPatch.osc1.pwmDepth = randNear( 0, 127, editPatch.osc1.pwmDepth );
    editPatch.osc1.pwmFreq = randNear( 0, 127, editPatch.osc1.pwmFreq );
    editPatch.osc1.sweepDepth = randNear( 0, 127, editPatch.osc1.sweepDepth );
    editPatch.osc1.sweepTime = randNear( 0, 127, editPatch.osc1.sweepTime );
    editPatch.osc1.breathAttain = randNear( 0, 127, editPatch.osc1.breathAttain );
    editPatch.osc1.breathDepth = randNear( 0, 127, editPatch.osc1.breathDepth );
    editPatch.osc1.breathCurve = randNear( 0, 127, editPatch.osc1.breathCurve );
    editPatch.osc1.breathThreshold = randNear( 0, 127, editPatch.osc1.breathThreshold );
    editPatch.osc1.level = randNear( 0, 127, editPatch.osc1.level );

    editPatch.osc2.fine = randNear( 0, 127, editPatch.osc2.fine );
    editPatch.osc2.beat = randNear( 0, 127, editPatch.osc2.beat );
    editPatch.osc2.sawtooth = randNear( 0, 127, editPatch.osc2.sawtooth );
    editPatch.osc2.triangle = randNear( 0, 127, editPatch.osc2.triangle );
    editPatch.osc2.square = randNear( 0, 127, editPatch.osc2.square );
    editPatch.osc2.pulseWidth = randNear( 0, 127, editPatch.osc2.pulseWidth );
    editPatch.osc2.pwmDepth = randNear( 0, 127, editPatch.osc2.pwmDepth );
    editPatch.osc2.pwmFreq = randNear( 0, 127, editPatch.osc2.pwmFreq );
    editPatch.osc2.sweepDepth = randNear( 0, 127, editPatch.osc2.sweepDepth );
    editPatch.osc2.sweepTime = randNear( 0, 127, editPatch.osc2.sweepTime );
    editPatch.osc2.breathAttain = randNear( 0, 127, editPatch.osc2.breathAttain );
    editPatch.osc2.breathDepth = randNear( 0, 127, editPatch.osc2.breathDepth );
    editPatch.osc2.breathCurve = randNear( 0, 127, editPatch.osc2.breathCurve );
    editPatch.osc2.breathThreshold = randNear( 0, 127, editPatch.osc2.breathThreshold );
    editPatch.osc2.level = randNear( 0, 127, editPatch.osc2.level );
    
    editPatch.oscFilter1.breathCurve = randNear( 0, 127, editPatch.oscFilter1.breathCurve );
    editPatch.oscFilter1.breathMod = randNear( 0, 127, editPatch.oscFilter1.breathMod );
    editPatch.oscFilter1.freq = randNear( 0, 127, editPatch.oscFilter1.freq );
    editPatch.oscFilter1.keyFollow = randNear( 0, 127, editPatch.oscFilter1.keyFollow );
    editPatch.oscFilter1.lfoBreath = randNear( 0, 127, editPatch.oscFilter1.lfoBreath );
    editPatch.oscFilter1.lfoDepth = randNear( 0, 127, editPatch.oscFilter1.lfoDepth );
    editPatch.oscFilter1.lfoFreq = randNear( 0, 127, editPatch.oscFilter1.lfoFreq );
    editPatch.oscFilter1.lfoThreshold = randNear( 0, 127, editPatch.oscFilter1.lfoThreshold );
    editPatch.oscFilter1.q = randNear( 5, 127, editPatch.oscFilter1.q );
    editPatch.oscFilter1.sweepDepth = randNear( 0, 127, editPatch.oscFilter1.sweepDepth );
    editPatch.oscFilter1.sweepTime = randNear( 0, 127, editPatch.oscFilter1.sweepTime );

    editPatch.oscFilter2.breathCurve = randNear( 0, 127, editPatch.oscFilter2.breathCurve );
    editPatch.oscFilter2.breathMod = randNear( 0, 127, editPatch.oscFilter2.breathMod );
    editPatch.oscFilter2.freq = randNear( 0, 127, editPatch.oscFilter2.freq );
    editPatch.oscFilter2.keyFollow = randNear( 0, 127, editPatch.oscFilter2.keyFollow );
    editPatch.oscFilter2.lfoBreath = randNear( 0, 127, editPatch.oscFilter2.lfoBreath );
    editPatch.oscFilter2.lfoDepth = randNear( 0, 127, editPatch.oscFilter2.lfoDepth );
    editPatch.oscFilter2.lfoFreq = randNear( 0, 127, editPatch.oscFilter2.lfoFreq );
    editPatch.oscFilter2.lfoThreshold = randNear( 0, 127, editPatch.oscFilter2.lfoThreshold );
    editPatch.oscFilter2.q = randNear( 5, 127, editPatch.oscFilter2.q );
    editPatch.oscFilter2.sweepDepth = randNear( 0, 127, editPatch.oscFilter2.sweepDepth );
    editPatch.oscFilter2.sweepTime = randNear( 0, 127, editPatch.oscFilter2.sweepTime );
    
    editPatch.chorusDelay1 = randNear( 0, 127, editPatch.chorusDelay1 );
    editPatch.chorusModLev1 = randNear( 0, 127, editPatch.chorusModLev1 );
    editPatch.chorusWetLev1 = randNear( 0, 127, editPatch.chorusWetLev1 );
    editPatch.chorusDelay2 = randNear( 0, 127, editPatch.chorusDelay2 );
    editPatch.chorusModLev2 = randNear( 0, 127, editPatch.chorusModLev2 );
    editPatch.chorusWetLev2 = randNear( 0, 127, editPatch.chorusWetLev2 );
    editPatch.chorusDryLevel = randNear( 0, 127, editPatch.chorusDryLevel );
    editPatch.chorusFeedback = randNear( 0, 127, editPatch.chorusFeedback );
    editPatch.chorusLFOfreq = randNear( 0, 127, editPatch.chorusLFOfreq );
    editPatch.delayTime = randNear( 0, 127, editPatch.delayTime );
    editPatch.delayDamp = randNear( 0, 127, editPatch.delayDamp );
    editPatch.delayFeedback = randNear( 0, 127, editPatch.delayFeedback );
    editPatch.delayDry = randNear( 0, 127, editPatch.delayDry );
    editPatch.delayLevel = randNear( 0, 127, editPatch.delayLevel );
    editPatch.reverbDamp = randNear( 0, 127, editPatch.reverbDamp );
    editPatch.reverbDensity = randNear( 0, 127, editPatch.reverbDensity );
    editPatch.reverbDry = randNear( 0, 127, editPatch.reverbDry );
    editPatch.reverbLevel = randNear( 0, 127, editPatch.reverbLevel );
    editPatch.reverbTime = randNear( 0, 127, editPatch.reverbTime );
    
    editPatch.encodeBlob();
    setAllControls();
    midiHandler.ignoreEvents = false;
    midiHandler.sendPatch( editPatch, EWI4000sPatch.EWI_EDIT );    
  }
  
  public void defaultPatch() {
    /* There is a risk of flooding the EWI if we change everything via CCs,
     * so we change the editPatch object and reset the UI.
     */
    midiHandler.ignoreEvents = true;
    uneditedPatch = editPatch;
    // just a plain triangle wave @ 75% volume
    editPatch.osc1.octave = 64;
    editPatch.osc1.semitone = 64;
    editPatch.osc1.fine = 64;
    editPatch.osc1.beat = 64;
    editPatch.osc1.sawtooth = 0;
    editPatch.osc1.triangle = 127;
    editPatch.osc1.square = 0;
    editPatch.osc1.pulseWidth = 64;
    editPatch.osc1.pwmDepth = 0;
    editPatch.osc1.pwmFreq = 0;
    editPatch.osc1.sweepDepth = 64;
    editPatch.osc1.sweepTime = 0;
    editPatch.osc1.breathAttain = 0;
    editPatch.osc1.breathDepth = 0;
    editPatch.osc1.breathCurve = 64;
    editPatch.osc1.breathThreshold = 0;
    editPatch.osc1.level = 96;

    // osc2 silent
    editPatch.osc2Xfade = 0;
    editPatch.osc2.octave = 64;
    editPatch.osc2.semitone = 64;
    editPatch.osc2.fine = 64;
    editPatch.osc2.beat = 64;
    editPatch.osc2.sawtooth = 0;
    editPatch.osc2.triangle = 0;
    editPatch.osc2.square = 0;
    editPatch.osc2.pulseWidth = 64;
    editPatch.osc2.pwmDepth = 0;
    editPatch.osc2.pwmFreq = 0;
    editPatch.osc2.sweepDepth = 64;
    editPatch.osc2.sweepTime = 0;
    editPatch.osc2.breathAttain = 0;
    editPatch.osc2.breathDepth = 0;
    editPatch.osc2.breathCurve = 64;
    editPatch.osc2.breathThreshold = 0;
    editPatch.osc2.level = 0;
    
    // no filtering
    editPatch.formantFilter = 0;
    editPatch.keyTrigger = 1;
    editPatch.oscFilterLink = 0; // ?
    
    editPatch.oscFilter1.mode = 4; // off
    editPatch.oscFilter1.breathCurve = 64;
    editPatch.oscFilter1.breathMod = 0;
    editPatch.oscFilter1.freq = 0;
    editPatch.oscFilter1.keyFollow = 64;
    editPatch.oscFilter1.lfoBreath = 64;
    editPatch.oscFilter1.lfoDepth = 0;
    editPatch.oscFilter1.lfoFreq = 0;
    editPatch.oscFilter1.lfoThreshold = 0;
    editPatch.oscFilter1.q = 5;
    editPatch.oscFilter1.sweepDepth = 64;
    editPatch.oscFilter1.sweepTime = 0;

    editPatch.oscFilter2.mode = 4; // off
    editPatch.oscFilter2.breathCurve = 64;
    editPatch.oscFilter2.breathMod = 0;
    editPatch.oscFilter2.freq = 0;
    editPatch.oscFilter2.keyFollow = 64;
    editPatch.oscFilter2.lfoBreath = 64;
    editPatch.oscFilter2.lfoDepth = 0;
    editPatch.oscFilter2.lfoFreq = 0;
    editPatch.oscFilter2.lfoThreshold = 0;
    editPatch.oscFilter2.q = 5;
    editPatch.oscFilter2.sweepDepth = 64;
    editPatch.oscFilter2.sweepTime = 0;
  
    editPatch.noiseBreath = 0;
    editPatch.noiseLevel = 0;
    editPatch.noiseTime = 0;
    
    editPatch.noiseFilter1.mode = 4; // off
    editPatch.noiseFilter1.breathCurve = 64;
    editPatch.noiseFilter1.breathMod = 0;
    editPatch.noiseFilter1.freq = 0;
    editPatch.noiseFilter1.keyFollow = 64;
    editPatch.noiseFilter1.lfoBreath = 64;
    editPatch.noiseFilter1.lfoDepth = 0;
    editPatch.noiseFilter1.lfoFreq = 0;
    editPatch.noiseFilter1.lfoThreshold = 0;
    editPatch.noiseFilter1.q = 5;
    editPatch.noiseFilter1.sweepDepth = 64;
    editPatch.noiseFilter1.sweepTime = 0;

    editPatch.noiseFilter2.mode = 4; // off
    editPatch.noiseFilter2.breathCurve = 64;
    editPatch.noiseFilter2.breathMod = 0;
    editPatch.noiseFilter2.freq = 0;
    editPatch.noiseFilter2.keyFollow = 64;
    editPatch.noiseFilter2.lfoBreath = 64;
    editPatch.noiseFilter2.lfoDepth = 0;
    editPatch.noiseFilter2.lfoFreq = 0;
    editPatch.noiseFilter1.lfoThreshold = 0;
    editPatch.noiseFilter2.q = 5;
    editPatch.noiseFilter2.sweepDepth = 64;
    editPatch.noiseFilter2.sweepTime = 0;
    
    editPatch.chorusSwitch = 0;
    editPatch.chorusDelay1 = 0;
    editPatch.chorusModLev1 = 0;
    editPatch.chorusWetLev1 = 0;
    editPatch.chorusDelay2 = 0;
    editPatch.chorusModLev2 = 0;
    editPatch.chorusWetLev2 = 0;
    editPatch.chorusDryLevel = 127;
    editPatch.chorusFeedback = 0;
    editPatch.chorusLFOfreq = 0;
    editPatch.delayTime = 0;
    editPatch.delayDamp = 0;
    editPatch.delayFeedback = 0;
    editPatch.delayDry = 127;
    editPatch.delayLevel = 0;
    editPatch.reverbDamp = 0;
    editPatch.reverbDensity = 0;
    editPatch.reverbDry = 127;
    editPatch.reverbLevel = 0;
    editPatch.reverbTime = 0;
    
    editPatch.biteTremolo = 0;
    editPatch.biteVibrato = 127;
    
    editPatch.ampLevel = 96; // 75%
    editPatch.octaveLevel = 36; // seems to be default
    
    editPatch.encodeBlob();
    setAllControls();
    midiHandler.ignoreEvents = false;
    midiHandler.sendPatch( editPatch, EWI4000sPatch.EWI_EDIT );    
  }
  
  public void randomPatch() {
    /* There is a risk of flooding the EWI if we change everything via CCs,
     * so we change the editPatch object and reset the UI.
     */
    midiHandler.ignoreEvents = true;
    uneditedPatch = editPatch;
    
    editPatch.osc1.octave = 64;
    editPatch.osc1.semitone = 64;

    editPatch.osc1.fine = randBetween( 0, 127 );
    editPatch.osc1.beat = randBetween( 0, 127 );
    editPatch.osc1.sawtooth = randBetween( 0, 127 );
    editPatch.osc1.triangle = randBetween( 0, 127 );
    editPatch.osc1.square = randBetween( 0, 127 );
    editPatch.osc1.pulseWidth = randBetween( 0, 127 );
    editPatch.osc1.pwmDepth = randBetween( 0, 127 );
    editPatch.osc1.pwmFreq = randBetween( 0, 127 );
    editPatch.osc1.sweepDepth = randBetween( 0, 127 );
    editPatch.osc1.sweepTime = randBetween( 0, 127 );
    editPatch.osc1.breathAttain = randBetween( 0, 127 );
    editPatch.osc1.breathDepth = randBetween( 0, 127 );
    editPatch.osc1.breathCurve = randBetween( 0, 127 );
    editPatch.osc1.breathThreshold = randBetween( 0, 127 );
    editPatch.osc1.level = randBetween( 48, 127 );

    editPatch.osc2Xfade = randBetween( 0, 1 );
    
    editPatch.osc1.octave = randBetween( 62, 66 );
    editPatch.osc1.semitone = 64;  // keep sane for now
    
    editPatch.osc2.fine = randBetween( 0, 127 );
    editPatch.osc2.beat = randBetween( 0, 127 );
    editPatch.osc2.sawtooth = randBetween( 0, 127 );
    editPatch.osc2.triangle = randBetween( 0, 127 );
    editPatch.osc2.square = randBetween( 0, 127 );
    editPatch.osc2.pulseWidth = randBetween( 0, 127 );
    editPatch.osc2.pwmDepth = randBetween( 0, 127 );
    editPatch.osc2.pwmFreq = randBetween( 0, 127 );
    editPatch.osc2.sweepDepth = randBetween( 0, 127 );
    editPatch.osc2.sweepTime = randBetween( 0, 127 );
    editPatch.osc2.breathAttain = randBetween( 0, 127 );
    editPatch.osc2.breathDepth = randBetween( 0, 127 );
    editPatch.osc2.breathCurve = randBetween( 0, 127 );
    editPatch.osc2.breathThreshold = randBetween( 0, 127 );
    editPatch.osc2.level = randBetween( 0, 127 );
    
    editPatch.formantFilter = randBetween( 0, 2 );
    editPatch.keyTrigger = randBetween( 0, 1 );
    editPatch.oscFilterLink = randBetween( 0, 2 );
    
    editPatch.oscFilter1.mode = randBetween( 0, 4 );
    editPatch.oscFilter1.breathCurve = randBetween( 0, 127 );
    editPatch.oscFilter1.breathMod = randBetween( 0, 127 );
    editPatch.oscFilter1.freq = randBetween( 0, 127 );
    editPatch.oscFilter1.keyFollow = randBetween( 0, 127 );
    editPatch.oscFilter1.lfoBreath = randBetween( 0, 127 );
    editPatch.oscFilter1.lfoDepth = randBetween( 0, 127 );
    editPatch.oscFilter1.lfoFreq = randBetween( 0, 127 );
    editPatch.oscFilter1.lfoThreshold = randBetween( 0, 127 );
    editPatch.oscFilter1.q = randBetween( 5, 127 );
    editPatch.oscFilter1.sweepDepth = randBetween( 0, 127 );
    editPatch.oscFilter1.sweepTime = randBetween( 0, 127 );

    editPatch.oscFilter2.mode = randBetween( 0, 4 );
    editPatch.oscFilter2.breathCurve = randBetween( 0, 127 );
    editPatch.oscFilter2.breathMod = randBetween( 0, 127 );
    editPatch.oscFilter2.freq = randBetween( 0, 127 );
    editPatch.oscFilter2.keyFollow = randBetween( 0, 127 );
    editPatch.oscFilter2.lfoBreath = randBetween( 0, 127 );
    editPatch.oscFilter2.lfoDepth = randBetween( 0, 127 );
    editPatch.oscFilter2.lfoFreq = randBetween( 0, 127 );
    editPatch.oscFilter2.lfoThreshold = randBetween( 0, 127 );
    editPatch.oscFilter2.q = randBetween( 5, 127 );
    editPatch.oscFilter2.sweepDepth = randBetween( 0, 127 );
    editPatch.oscFilter2.sweepTime = randBetween( 0, 127 );
    
    editPatch.noiseBreath = randBetween( 0, 127 );
    editPatch.noiseLevel = randBetween( 0, 127 );
    editPatch.noiseTime = randBetween( 0, 127 );
    
    // TODO add noise filters
    
    editPatch.chorusSwitch = randBetween( 0, 1 );
    editPatch.chorusDelay1 = randBetween( 0, 127 );
    editPatch.chorusModLev1 = randBetween( 0, 127 );
    editPatch.chorusWetLev1 = randBetween( 0, 127 );
    editPatch.chorusDelay2 = randBetween( 0, 127 );
    editPatch.chorusModLev2 = randBetween( 0, 127 );
    editPatch.chorusWetLev2 = randBetween( 0, 127 );
    editPatch.chorusDryLevel = randBetween( 0, 127 );
    editPatch.chorusFeedback = randBetween( 0, 127 );
    editPatch.chorusLFOfreq = randBetween( 0, 127 );
    editPatch.delayTime = randBetween( 0, 127 );
    editPatch.delayDamp = randBetween( 0, 127 );
    editPatch.delayFeedback = randBetween( 0, 127 );
    editPatch.delayDry = randBetween( 64, 127 );
    editPatch.delayLevel = randBetween( 0, 127 );
    editPatch.reverbDamp = randBetween( 0, 127 );
    editPatch.reverbDensity = randBetween( 0, 127 );
    editPatch.reverbDry = randBetween( 64, 127 );
    editPatch.reverbLevel = randBetween( 0, 12 );
    editPatch.reverbTime = randBetween( 0, 127 );
    
    editPatch.encodeBlob();
    setAllControls();
    midiHandler.ignoreEvents = false;
    midiHandler.sendPatch( editPatch, EWI4000sPatch.EWI_EDIT );     
  }
  
  public void mergePatchesUi() {
    List<String> patchNames = new ArrayList<>();
    for (int p = 0; p < EWI4000sPatch.EWI_NUM_PATCHES; p++)
      patchNames.add( sharedData.ewiPatchList[p].getName() );
    Dialog<Integer> mergeDialog = new Dialog<>();
    mergeDialog.setTitle( "EWItool - Merge Patches" );
    mergeDialog.setHeaderText( "Choose patch to merge with..." );
    ListView<String> lv  = new ListView<>();
    lv.getItems().setAll( patchNames ); // don't need Observable here
    mergeDialog.getDialogPane().setContent( lv );
    mergeDialog.setResultConverter( button -> { 
      if (button == ButtonType.OK)
        return lv.getSelectionModel().getSelectedIndex();
      else
        return null;
    } );
    mergeDialog.getDialogPane().getButtonTypes().addAll( ButtonType.OK, ButtonType.CANCEL );
    Optional<Integer> result = mergeDialog.showAndWait();
    if (result.isPresent() && result.get() != -1) {
      mergeWith( result.get() );    
    }
  }
  
  private void mergeWith( int otherPatchNum ) {
    /* There is a risk of flooding the EWI if we change everything via CCs,
     * so we change the editPatch object and reset the UI.
     */
    midiHandler.ignoreEvents = true;
    uneditedPatch = editPatch;
    EWI4000sPatch tmpPatch = sharedData.ewiPatchList[otherPatchNum];
    
    editPatch.osc1.octave = mixInts( editPatch.osc1.octave, 50, tmpPatch.osc1.octave  );
    editPatch.osc1.semitone = mixInts( editPatch.osc1.semitone, 50, tmpPatch.osc1.semitone  );

    editPatch.osc1.fine = mixInts( editPatch.osc1.fine, 50, tmpPatch.osc1.fine  );
    editPatch.osc1.beat = mixInts( editPatch.osc1.beat, 50, tmpPatch.osc1.beat);
    editPatch.osc1.sawtooth = mixInts( editPatch.osc1.sawtooth, 50, tmpPatch.osc1.sawtooth);
    editPatch.osc1.triangle = mixInts( editPatch.osc1.triangle, 50, tmpPatch.osc1.triangle);
    editPatch.osc1.square = mixInts( editPatch.osc1.square, 50, tmpPatch.osc1.square);
    editPatch.osc1.pulseWidth = mixInts( editPatch.osc1.pulseWidth, 50, tmpPatch.osc1.pulseWidth);
    editPatch.osc1.pwmDepth = mixInts( editPatch.osc1.pwmDepth, 50, tmpPatch.osc1.pwmDepth);
    editPatch.osc1.pwmFreq = mixInts( editPatch.osc1.pwmFreq, 50, tmpPatch.osc1.pwmFreq);
    editPatch.osc1.sweepDepth = mixInts( editPatch.osc1.sweepDepth, 50, tmpPatch.osc1.sweepDepth);
    editPatch.osc1.sweepTime = mixInts( editPatch.osc1.sweepTime, 50, tmpPatch.osc1.sweepTime);
    editPatch.osc1.breathAttain = mixInts( editPatch.osc1.breathAttain, 50, tmpPatch.osc1.breathAttain);
    editPatch.osc1.breathDepth = mixInts( editPatch.osc1.breathDepth, 50, tmpPatch.osc1.breathDepth);
    editPatch.osc1.breathCurve = mixInts( editPatch.osc1.breathCurve, 50, tmpPatch.osc1.breathCurve);
    editPatch.osc1.breathThreshold = mixInts( editPatch.osc1.breathThreshold, 50, tmpPatch.osc1.breathThreshold);
    editPatch.osc1.level = mixInts( editPatch.osc1.level, 50, tmpPatch.osc1.level);

    //editPatch.osc2Xfade = mixInts( 0, 1 );
    
    editPatch.osc1.octave = mixInts( editPatch.osc1.octave, 50, tmpPatch.osc1.octave);
    editPatch.osc1.semitone = mixInts( editPatch.osc1.semitone, 50, tmpPatch.osc1.semitone); 
    
    editPatch.osc2.fine = mixInts( editPatch.osc2.fine, 50, tmpPatch.osc2.fine);
    editPatch.osc2.beat = mixInts( editPatch.osc2.beat, 50, tmpPatch.osc2.beat);
    editPatch.osc2.sawtooth = mixInts( editPatch.osc2.sawtooth, 50, tmpPatch.osc2.sawtooth);
    editPatch.osc2.triangle = mixInts( editPatch.osc2.triangle, 50, tmpPatch.osc2.triangle);
    editPatch.osc2.square = mixInts( editPatch.osc2.square, 50, tmpPatch.osc2.square);
    editPatch.osc2.pulseWidth = mixInts( editPatch.osc2.pulseWidth, 50, tmpPatch.osc2.pulseWidth);
    editPatch.osc2.pwmDepth = mixInts( editPatch.osc2.pwmDepth, 50, tmpPatch.osc2.pwmDepth);
    editPatch.osc2.pwmFreq = mixInts( editPatch.osc2.pwmFreq, 50, tmpPatch.osc2.pwmFreq);
    editPatch.osc2.sweepDepth = mixInts( editPatch.osc2.sweepDepth, 50, tmpPatch.osc2.sweepDepth);
    editPatch.osc2.sweepTime = mixInts( editPatch.osc2.sweepTime, 50, tmpPatch.osc2.sweepTime);
    editPatch.osc2.breathAttain = mixInts( editPatch.osc2.breathAttain, 50, tmpPatch.osc2.breathAttain);
    editPatch.osc2.breathDepth = mixInts( editPatch.osc2.breathDepth, 50, tmpPatch.osc2.breathDepth);
    editPatch.osc2.breathCurve = mixInts( editPatch.osc2.breathCurve, 50, tmpPatch.osc2.breathCurve);
    editPatch.osc2.breathThreshold = mixInts( editPatch.osc2.breathThreshold, 50, tmpPatch.osc2.breathThreshold);
    editPatch.osc2.level = mixInts( editPatch.osc2.level, 50, tmpPatch.osc2.level);
    
    //editPatch.formantFilter = mixInts( editPatch.formantFilter, 50, tmpPatch.formantFilter);
    //editPatch.keyTrigger = mixInts( 0, 1 );
    //editPatch.oscFilterLink = mixInts( 0, 50, tmpPatch.);
    
    //editPatch.oscFilter1.mode = mixInts( 0, 4 );
    editPatch.oscFilter1.breathCurve = mixInts( editPatch.oscFilter1.breathCurve, 50, tmpPatch.oscFilter1.breathCurve);
    editPatch.oscFilter1.breathMod = mixInts( editPatch.oscFilter1.breathMod, 50, tmpPatch.oscFilter1.breathMod);
    editPatch.oscFilter1.freq = mixInts( editPatch.oscFilter1.freq, 50, tmpPatch.oscFilter1.freq);
    editPatch.oscFilter1.keyFollow = mixInts( editPatch.oscFilter1.keyFollow, 50, tmpPatch.oscFilter1.keyFollow);
    editPatch.oscFilter1.lfoBreath = mixInts( editPatch.oscFilter1.lfoBreath, 50, tmpPatch.oscFilter1.lfoBreath);
    editPatch.oscFilter1.lfoDepth = mixInts( editPatch.oscFilter1.lfoDepth, 50, tmpPatch.oscFilter1.lfoDepth);
    editPatch.oscFilter1.lfoFreq = mixInts( editPatch.oscFilter1.lfoFreq, 50, tmpPatch.oscFilter1.lfoFreq);
    editPatch.oscFilter1.lfoThreshold = mixInts( editPatch.oscFilter1.lfoThreshold, 50, tmpPatch.oscFilter1.lfoThreshold);
    editPatch.oscFilter1.q = mixInts( editPatch.oscFilter1.q, 50, tmpPatch.oscFilter1.q);
    editPatch.oscFilter1.sweepDepth = mixInts( editPatch.oscFilter1.sweepDepth, 50, tmpPatch.oscFilter1.sweepDepth);
    editPatch.oscFilter1.sweepTime = mixInts( editPatch.oscFilter1.sweepTime, 50, tmpPatch.oscFilter1.sweepTime);

    //editPatch.oscFilter2.mode = mixInts( editPatch.oscFilter2.mode, 4 oscFilter2.mode);
    editPatch.oscFilter2.breathCurve = mixInts( editPatch.oscFilter2.breathCurve, 50, tmpPatch.oscFilter2.breathCurve);
    editPatch.oscFilter2.breathMod = mixInts( editPatch.oscFilter2.breathMod, 50, tmpPatch.oscFilter2.breathMod);
    editPatch.oscFilter2.freq = mixInts( editPatch.oscFilter2.freq, 50, tmpPatch.oscFilter2.freq);
    editPatch.oscFilter2.keyFollow = mixInts( editPatch.oscFilter2.keyFollow, 50, tmpPatch.oscFilter2.keyFollow);
    editPatch.oscFilter2.lfoBreath = mixInts( editPatch.oscFilter2.lfoBreath, 50, tmpPatch.oscFilter2.lfoBreath);
    editPatch.oscFilter2.lfoDepth = mixInts( editPatch.oscFilter2.lfoDepth, 50, tmpPatch.oscFilter2.lfoDepth);
    editPatch.oscFilter2.lfoFreq = mixInts( editPatch.oscFilter2.lfoFreq, 50, tmpPatch.oscFilter2.lfoFreq);
    editPatch.oscFilter2.lfoThreshold = mixInts( editPatch.oscFilter2.lfoThreshold, 50, tmpPatch.oscFilter2.lfoThreshold);
    editPatch.oscFilter2.q = mixInts( editPatch.oscFilter2.q, 50, tmpPatch.oscFilter2.q);
    editPatch.oscFilter2.sweepDepth = mixInts( editPatch.oscFilter2.sweepDepth, 50, tmpPatch.oscFilter2.sweepDepth);
    editPatch.oscFilter2.sweepTime = mixInts( editPatch.oscFilter2.sweepTime, 50, tmpPatch.oscFilter2.sweepTime);
    
    editPatch.noiseBreath = mixInts( editPatch.noiseBreath, 50, tmpPatch.noiseBreath);
    editPatch.noiseLevel = mixInts( editPatch.noiseLevel, 50, tmpPatch.noiseLevel);
    editPatch.noiseTime = mixInts( editPatch.noiseTime, 50, tmpPatch.noiseTime);
    
    // TODO add noise filters
    
   // editPatch.chorusSwitch = mixInts( 0, 1 );
    editPatch.chorusDelay1 = mixInts( editPatch.chorusDelay1, 50, tmpPatch.chorusDelay1);
    editPatch.chorusModLev1 = mixInts( editPatch.chorusModLev1, 50, tmpPatch.chorusModLev1);
    editPatch.chorusWetLev1 = mixInts( editPatch.chorusWetLev1, 50, tmpPatch.chorusWetLev1);
    editPatch.chorusDelay2 = mixInts( editPatch.chorusDelay2, 50, tmpPatch.chorusDelay2);
    editPatch.chorusModLev2 = mixInts( editPatch.chorusModLev2, 50, tmpPatch.chorusModLev2);
    editPatch.chorusWetLev2 = mixInts( editPatch.chorusWetLev2, 50, tmpPatch.chorusWetLev2);
    editPatch.chorusDryLevel = mixInts( editPatch.chorusDryLevel, 50, tmpPatch.chorusDryLevel);
    editPatch.chorusFeedback = mixInts( editPatch.chorusFeedback, 50, tmpPatch.chorusFeedback);
    editPatch.chorusLFOfreq = mixInts( editPatch.chorusLFOfreq, 50, tmpPatch.chorusLFOfreq);
    editPatch.delayTime = mixInts( editPatch.delayTime, 50, tmpPatch.delayTime);
    editPatch.delayDamp = mixInts( editPatch.delayDamp, 50, tmpPatch.delayDamp);
    editPatch.delayFeedback = mixInts( editPatch.delayFeedback, 50, tmpPatch.delayFeedback);
    editPatch.delayDry = mixInts( editPatch.delayDry, 50, tmpPatch.delayDry);
    editPatch.delayLevel = mixInts( editPatch.delayLevel, 50, tmpPatch.delayLevel);
    editPatch.reverbDamp = mixInts( editPatch.reverbDamp, 50, tmpPatch.reverbDamp);
    editPatch.reverbDensity = mixInts( editPatch.reverbDensity, 50, tmpPatch.reverbDensity);
    editPatch.reverbDry = mixInts( editPatch.reverbDry, 50, tmpPatch.reverbDry);
    editPatch.reverbLevel = mixInts( editPatch.reverbLevel, 50, tmpPatch.reverbLevel);
    editPatch.reverbTime = mixInts( editPatch.reverbTime, 50, tmpPatch.reverbTime);
    
    editPatch.encodeBlob();
    setAllControls();
    midiHandler.ignoreEvents = false;
    midiHandler.sendPatch( editPatch, EWI4000sPatch.EWI_EDIT ); 
  }
  
  /*
   * return a psuedorandom integer between min and max
   */
  private int randBetween( int min, int max ) {
    return min + (int)((max - min) * Math.random());
  }
  
  /*
   * return current value psuedorandomly modified by up to 10% within specified bounds
   */
  private int randNear( int min, int max, int currval ) {
    int newmin = currval - (max/10); 
    if (newmin < min) newmin = min;
    if (newmin == max) newmin -= max/10;  
    
    int newmax = currval + (max/10);
    if (newmax > max) newmax = max;
    if (newmax == min) newmax += max/10;
    
    return randBetween( newmin, newmax );
  }
  
  /**
   * Returns pct of p1 plus 1-pct of p2 i.e. avergage if pct == 50
   * @param p1
   * @param pct
   * @param p2
   * @return
   */
  private int mixInts( int p1, int pct, int p2 ) {
    return ((p1 * pct) / 100) + ((p2 * (100-pct)) / 100 );
  }
}

// helper classes used for (dynamically) labelling the controls
class ControlLabel extends Label {
  ControlLabel( String lab, HPos pos ) {
    setText( lab );
    setId( "editor-control-label" );
    GridPane.setHalignment( this, pos );
  }
}
class BoundRightControlLabel extends Label {
  BoundRightControlLabel( String lab, HPos pos, Slider sl ) {
    setId( "editor-control-label" );
    setTextAlignment( TextAlignment.CENTER );
    GridPane.setHalignment( this, pos );
    textProperty().bind( Bindings.format( lab + " %.0f", sl.valueProperty()) );
  }
}
class BoundBelowControlLabel extends Label {
  BoundBelowControlLabel( String lab, HPos pos, Slider sl ) {
    setId( "editor-control-label" );
    setTextAlignment( TextAlignment.CENTER );
    GridPane.setHalignment( this, pos );
    textProperty().bind( Bindings.format( lab + "%n%.0f", sl.valueProperty()) );
  }
}
class GroupLabel extends Label {
  GroupLabel( String lab ) {
    setText( lab );
    setId( "editor-group-label" );
    GridPane.setHalignment( this, HPos.CENTER );
  }
}