#include <stdint.h>
#include "WSerial.h"
#include "usbd_def.h"
#ifndef Ui_h
#define Ui_h

#include "Adafruit_SH110X.h"
#include "Adafruit_GFX.h"
#include "potentiometers.h"
#include "buttons.h"
#include "looper.h"
#include "button_grid.h"

#define UI_MAX_FPS 10
#define WAVEFORM_OVERSAMPLING 2
#define WAVEFORM_LIN true

extern Potentiometer pot_1, pot_2, pot_3, pot_4, pot_5, pot_6, pot_7;
extern Button button_1, button_2, button_3, button_4, button_5, button_6;
extern Adafruit_SH1106G display;
extern atoav::Looper looper_a, looper_b, looper_c, looper_d, looper_e;

// Should the splash-screen be shown on boot?
bool show_splash = false;

// Represents the possible states of the UI
enum UiMode {
  UI_MODE_SPLASH,         // A splash screen that is shown on startup
  UI_MODE_DEFAULT,        // Default screen: Show Waveform and Parameters
  UI_MODE_REC_MENU,       // ButtonGrid Menu: Recording Settings
  UI_MODE_PLAY_MENU,      // ButtonGrid Menu: Playback Settings
  UI_MODE_TRIGGER_MENU,   // ButtonGrid Menu: Trigger Settings
  UI_MODE_FX_MENU,        // ButtonGrid Menu: FX Settings
  UI_MODE_BUFFER_MENU,    // ButtonGrid Menu: Buffer Settings
  UI_MODE_LAST
};

// Represents possible recording modes
enum RecMode {
  REC_MODE_FULL,          // Record into full buffer (looping back to start)
  REC_MODE_LOOP,          // Limit recording to the loop
  REC_MODE_FULL_SHOT,     // Record into full buffer, stop at the end
  REC_MODE_LAST
};

// Represents possible recording sources
enum RecSource {
  REC_SOURCE_PRE,            // Record Incoming audio
  REC_SOURCE_POST,           // Record effects
  REC_SOURCE_OUT,            // Record the buffer output
  REC_SOURCE_NOISE,          // Record Noise
  REC_SOURCE_LAST
};

// Represents possible playback modes
enum PlayMode {
  PLAY_MODE_DRUNK,        // Drunken Walk
  PLAY_MODE_WINDOW,       // Sliding window
  PLAY_MODE_LOOP,         // Loop
  PLAY_MODE_GRAIN,        // Granular ?
  PLAY_MODE_ONESHOT,      // Play it once
  PLAY_MODE_LAST
};

// Represents possible recording states
enum RecordingState {
  REC_STATE_NOT_RECORDING, // Not recording
  REC_STATE_RECORDING,     // Recording (replace what is in the buffer)
  REC_STATE_OVERDUBBING,   // Overdubbing (mix recorded values with the existing samples)
  REC_STATE_LAST
};

enum ActiveBuffer {
  ACTIVE_BUFFER_A,
  ACTIVE_BUFFER_B,
  ACTIVE_BUFFER_C,
  ACTIVE_BUFFER_D,
  ACTIVE_BUFFER_E,
  ACTIVE_BUFFER_LAST,
};

enum BufferSummingMode {
  BUFFER_SUM_MODE_SOLO,
  BUFFER_SUM_MODE_SUM,
  BUFFER_SUM_MODE_RING,
  BUFFER_SUM_MODE_LAST,
};

enum FXMode {
  FX_MODE_ALL,
  FX_MODE_REVERB,
  FX_MODE_NONE,
  FX_MODE_LFO,
  FX_MODE_GRAIN,
  FX_MODE_EMPTY,
  FX_MODE_LAST,
};

// The Ui is _the_ coordinating class for the whole interaction.
// The default mode
// Note Descriptions get a space of 21 chars and 8 lines
class Ui {
  public:
    Ui() : button_grids {
      ButtonGrid((int) UI_MODE_REC_MENU, {
        GridButton("REC\nMENU", &button_1, true),
        GridButton("MOM\nTOGGLE", &button_2, false, BUTTON_TYPE_TOGGLE, 0, "REC/OVERDUB BUTTONS\n\nMOM->Record while    \n     Button is held\n\nTOGGLE->Press once to\n        start, press \n        again to stop"),
        GridButton("PRE\nPOST\nOUT\nNOISE", &button_3, false, BUTTON_TYPE_MULTITOGGLE, 0, "REC/OVERDUB SOURCE\n\nPRE----->Direct Input\nPOST---->With Effects\nOUT---->Looper Output\nNOISE--->Noise Source"),
        GridButton("FULL\nLOOP\nSHOT", &button_4, false, BUTTON_TYPE_MULTITOGGLE, 1, "RECORDING REGION\n\nFULL---->Whole Buffer\nLOOP----->Loop Bounds\nSHOT->Full Buffer but\n      stop at the end"),
        GridButton("NORMAL\nPITCHD\nUNPTCH", &button_5, false, BUTTON_TYPE_MULTITOGGLE, 0, "SPEED OF THE REC HEAD\n\nNORMAL--->Fixed Speed\nPITCHD->Inverse Playh\nUNPITCH----->Playhead\n             Speed"),
        GridButton("START\nLOOPST\nPLAYHD", &button_6, false, BUTTON_TYPE_MULTITOGGLE, 0, "START RECORDING AT\n\nSTART--->Start of the\n         Buffer\nLOOP---->Start of the\n         Loop\nPLAYH--->Position of\n         the Playhead"),
      }),
      ButtonGrid((int) UI_MODE_PLAY_MENU, {
        GridButton("STOP\nLOOP\nMULTI\nMIDI", &button_1, false, BUTTON_TYPE_MULTITOGGLE, 1),
        GridButton("PLAY\nMENU", &button_2, true),
        GridButton("ACTIVE\nSUM\nRING", &button_3, false, BUTTON_TYPE_MULTITOGGLE, 0),
        GridButton(" ", &button_4, false),
        GridButton(" ", &button_5, false),
        GridButton(" ", &button_6, false),
      }),
      ButtonGrid((int) UI_MODE_TRIGGER_MENU, {
        GridButton("MIDI\nTRIG.", &button_1, false),
        GridButton("MIDI\nUNMUTE", &button_2, false),
        GridButton("TRIG.\nMENU", &button_3, true),
        GridButton("MANUAL\nTRIG.", &button_4, false),
        GridButton("MANUAL\nUNMUTE", &button_5, false),
        GridButton("AUTO", &button_6, false),
      }),
      ButtonGrid((int) UI_MODE_FX_MENU, {
        GridButton("ALL", &button_1, false, BUTTON_TYPE_ENUM, 1),
        GridButton("REVERB", &button_2, false, BUTTON_TYPE_ENUM, 0),
        GridButton("FX\nMENU", &button_3, true),
        GridButton("LFO", &button_4, false, BUTTON_TYPE_ENUM, 0),
        GridButton("GRAIN", &button_5, false, BUTTON_TYPE_ENUM, 0),
        GridButton("-", &button_6, false, BUTTON_TYPE_ENUM, 0),
      }),
      ButtonGrid((int) UI_MODE_BUFFER_MENU, {
        GridButton("A", &button_1, false, BUTTON_TYPE_ENUM, 1),
        GridButton("B", &button_2, false, BUTTON_TYPE_ENUM, 0),
        GridButton("C", &button_3, false, BUTTON_TYPE_ENUM, 0),
        GridButton("D", &button_4, false, BUTTON_TYPE_ENUM, 0),
        GridButton("E", &button_5, false, BUTTON_TYPE_ENUM, 0),
        GridButton("BUFFER\nMENU", &button_6, true),
      }),
    } {};

    // Store the Button Grids declared above (make sure the lenght matches!)
    ButtonGrid button_grids[5];

    // Stores the current Ui Mode
    UiMode ui_mode = UI_MODE_SPLASH;

    // Default Recording Mode
    RecMode rec_mode = REC_MODE_LOOP;

    // Default Recording Source
    RecSource rec_source = REC_SOURCE_PRE;

    // Default active buffer
    ActiveBuffer active_buffer = ACTIVE_BUFFER_A;

    // Default active summing mode
    BufferSummingMode buffer_summing_mode = BUFFER_SUM_MODE_SOLO;

    FXMode fx_mode = FX_MODE_ALL;

    // Render the UI
    // Except for the splash screen this is expected to be called
    // repeatedly in a loop
    void Render() {
      double now = millis();
      // Serial.println(1000.0/UI_MAX_FPS);
      if ((now - last_render) > (1000.0/UI_MAX_FPS)) {
        switch (ui_mode) {
          case UI_MODE_SPLASH:
            renderSplash();
            break;
          case UI_MODE_DEFAULT:
            renderDefault();
            break;
          case UI_MODE_REC_MENU:
            renderGrid(0, rec_mode);
            break;
          case UI_MODE_PLAY_MENU:
            renderGrid(1);
            break;
          case UI_MODE_TRIGGER_MENU:
            renderGrid(2);
            break;
          case UI_MODE_FX_MENU:
            renderGrid(3, fx_mode);
            break;
          case UI_MODE_BUFFER_MENU:
            renderGrid(4, active_buffer);
            break;
        }
        last_render = now;
      }
    }

    // Helper method to render a certain button grid
    void renderGrid(size_t num, int button_enum=0) {
      display.clearDisplay();
      button_grids[num].render(button_enum);
      display.display();
    }

    // Renders a splash screen (runs once)
    void renderSplash() {
      display.setTextSize(1);
      if (show_splash) {
        display.setTextColor(SH110X_BLACK);
        // Play a fancy intro splash screen
        for (int i=0; i < 91; i++) {
          display.clearDisplay();
          display.fillCircle(display.width()/2, display.height()/2, i, SH110X_WHITE);
          display.fillCircle(display.width()/2, display.height()/2, max(0, i-2), SH110X_BLACK);
          if (i < 50) {
            centeredText("DAISYY", display.width()/2, display.height()/2-4, SH110X_WHITE);
            centeredText("LOOPER", display.width()/2, display.height()/2+4, SH110X_WHITE);
          }
          
          display.display();
          delay(1);
        }
        display.invertDisplay(true);
        display.display();
        delay(800);
        display.clearDisplay();
        display.invertDisplay(false);
        display.display();
      }

      // Splash rendering is now done, go to next UI Mode
      setMode(UI_MODE_DEFAULT);
    }

    // Helper method to reset the controls
    void resetControls() {
      button_1.reset();
      button_2.reset();
      button_3.reset();
      button_4.reset();
      button_5.reset();
      button_6.reset();
    }

    Button* setupButtonGrid(int n) {
      // Find the index of the home button
      int home_button_index = button_grids[n].homeButtonIndex();

      // Create a pointer to the hoime button
      Button* home_button = button_grids[n].grid_buttons_[home_button_index].button;

      // Reset the controls
      resetControls();

      // Setup the button grid
      button_grids[n].setup();

      // Return to default mode on release
      home_button->onReleased([this, n](){
        this->setMode(UI_MODE_DEFAULT);
        this->button_grids[n].hideAllDescriptions();
      });

      // Return pointer to the home button
      return home_button;
    }

    // Setup the Recording Menu
    void setupRecMenu() {
      // Only run once when the ui_mode changed
      if (ui_mode == UI_MODE_REC_MENU && last_ui_mode != UI_MODE_REC_MENU) {
        int n = 0;

        // Setup button Grid
        Button* home_button = setupButtonGrid(n);

        // Toggle between momentary and toggle recording modes
        button_2.onPress([this, n](){
          rec_button_momentary = !rec_button_momentary;
          button_grids[n].grid_buttons_[1].active = !rec_button_momentary;
        });
        
        // Set Recording Source (Pre/Post/Out/Noise)
        button_3.onPress([this, n](){
          button_grids[n].grid_buttons_[2].next();
          rec_source = (RecSource) button_grids[n].grid_buttons_[2].active;
        }); 

        // Switch Recording modes (Full/Loop/Oneshot)
        button_4.onPress([this, n](){ 
          button_grids[n].grid_buttons_[3].next();
          // Button.active returns number according to mode, we cast it to a RecMode enum
          rec_mode = (RecMode) button_grids[n].grid_buttons_[3].active;
          switch (rec_mode) {
            case REC_MODE_FULL: 
              looper_a.setRecModeFull(); 
              looper_b.setRecModeFull(); 
              looper_c.setRecModeFull(); 
              looper_d.setRecModeFull(); 
              looper_e.setRecModeFull(); 
              break;
            case REC_MODE_LOOP: 
              looper_a.setRecModeLoop();
              looper_b.setRecModeLoop();
              looper_c.setRecModeLoop();
              looper_d.setRecModeLoop();
              looper_e.setRecModeLoop();
              break;
            case REC_MODE_FULL_SHOT:
              looper_a.setRecModeFullShot();
              looper_b.setRecModeFullShot();
              looper_c.setRecModeFullShot();
              looper_d.setRecModeFullShot();
              looper_e.setRecModeFullShot();
              break;
          }
        });

        // Set Recording Pitch mode (Normal/Pitched/Unpitched)
        button_5.onPress([this, n](){ 
          button_grids[n].grid_buttons_[4].next();
          looper_a.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active);
          looper_b.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active);
          looper_c.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active);
          looper_d.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active);
          looper_e.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active);
        });

        // Set Recording Start Option (Buffer Start/Loop Start/Playhead)
        button_6.onPress([this, n](){
          button_grids[n].grid_buttons_[5].next();
          looper_a.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active);
          looper_b.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active);
          looper_c.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active);
          looper_d.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active);
          looper_e.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active);
        });

        // Store the last ui mode, for the check on top
        last_ui_mode = ui_mode;
      }
    }

    // Setup the Buffer Menu
    void setupBufferMenu() {
      // Only run once when the ui_mode changed
      if (ui_mode == UI_MODE_BUFFER_MENU && last_ui_mode != UI_MODE_BUFFER_MENU) {
        int n = 4;

        // Setup button Grid
        Button* home_button = setupButtonGrid(n);

        button_1.onPress([this, n](){ 
          active_buffer = ACTIVE_BUFFER_A;
          waveform_cache_dirty = true;
        });
        button_2.onPress([this, n](){ 
          active_buffer = ACTIVE_BUFFER_B;
          waveform_cache_dirty = true;
        });
        button_3.onPress([this, n](){ 
          active_buffer = ACTIVE_BUFFER_C;
          waveform_cache_dirty = true;
        });
        button_4.onPress([this, n](){ 
          active_buffer = ACTIVE_BUFFER_D;
          waveform_cache_dirty = true;
        });
        button_5.onPress([this, n](){ 
          active_buffer = ACTIVE_BUFFER_E;
          waveform_cache_dirty = true;
        });

        // Store the last ui mode, for the check on top
        last_ui_mode = ui_mode;
      }
    }

    // Setup the Play Menu
    void setupPlayMenu() {
      // Only run once when the ui_mode changed
      if (ui_mode == UI_MODE_PLAY_MENU && last_ui_mode != UI_MODE_PLAY_MENU) {
        int n = 1;

        // Ensure the UI showes the play mode of the active looper
        button_grids[n].grid_buttons_[0].active = (int) activeLooper()->playback_state ;

        // Setup button Grid
        Button* home_button = setupButtonGrid(n);

        // Chang the way in which buffers are summed
        button_3.onPress([this, n](){ 
          button_grids[n].grid_buttons_[2].next();
          buffer_summing_mode = (BufferSummingMode) button_grids[n].grid_buttons_[2].active;
        });

        // Change playback state (mode) of the current looper
        button_1.onPress([this, n](){ 
          button_grids[n].grid_buttons_[0].next();
          activeLooper()->playback_state = (atoav::PlaybackState) (button_grids[n].grid_buttons_[0].active);
        });

        // Store the last ui mode, for the check on top
        last_ui_mode = ui_mode;
      }
    }

    // Setup the FX Menu
    void setupFXMenu() {
      // Only run once when the ui_mode changed
      if (ui_mode == UI_MODE_FX_MENU && last_ui_mode != UI_MODE_FX_MENU) {
        int n = 3;

        // Ensure the UI showes the play mode of the active looper
        // button_grids[n].grid_buttons_[0].active = (int) fx_mode;

        // Setup button Grid
        Button* home_button = setupButtonGrid(n);

        // Select the active Effect (All)
        button_1.onPress([this, n](){ 
          fx_mode = FX_MODE_ALL;
          pot_5.setDisplayMode("LFO", 100.0f, POT_DISPLAY_MODE_PERCENT);
          pot_5.setLinear();
          pot_6.setDisplayMode("Delay", 100.0f, POT_DISPLAY_MODE_PERCENT);
          pot_7.setDisplayMode("Reverb", 100.0f, POT_DISPLAY_MODE_PERCENT);
        });

        // Select the active Effect (Reverb)
        button_2.onPress([this, n](){ 
          fx_mode = FX_MODE_REVERB;
          pot_5.setDisplayMode("Rev. Tone", 100.0f, POT_DISPLAY_MODE_PERCENT);
          pot_5.setLinear();
          pot_6.setDisplayMode("Rev. Decay", 100.0f, POT_DISPLAY_MODE_PERCENT);
          pot_7.setDisplayMode("Reverb Mix", 100.0f, POT_DISPLAY_MODE_PERCENT);
        });

        // Select the active Effect (LFO)
        button_4.onPress([this, n](){ 
          fx_mode = FX_MODE_LFO;
          pot_5.setDisplayMode("LFO Mode", 100.0f, POT_DISPLAY_MODE_SWITCH);
          pot_5.setSwitch();
          pot_5.switch_positions = 2;
          pot_5.switch_offset = 0;
          pot_6.setDisplayMode("LFO Speed", 100.0f, POT_DISPLAY_MODE_PERCENT);
          pot_7.setDisplayMode("LFO Amount", 100.0f, POT_DISPLAY_MODE_PERCENT);
        });

        // Select the active Effect (GRAIN)
        button_5.onPress([this, n](){ 
          fx_mode = FX_MODE_GRAIN;
          pot_5.setDisplayMode("Grain Num", 100.0f, POT_DISPLAY_MODE_SWITCH_NUMBERS);
          pot_5.setSwitch();
          pot_5.switch_positions = 8;
          pot_6.setDisplayMode("Grain Spread", 100.0f, POT_DISPLAY_MODE_PERCENT);
          pot_7.setDisplayMode("Grain Var.", 100.0f, POT_DISPLAY_MODE_PERCENT);
        });

        // Store the last ui mode, for the check on top
        last_ui_mode = ui_mode;
      }
    }

    // Setup the default (waveform) screen
    void setupDefault() {
      // Only run once on mode change
      if (ui_mode == UI_MODE_DEFAULT && last_ui_mode != UI_MODE_DEFAULT) {
        // Reset controls
        resetControls();

        // Set up the initial recording mode
        switch (rec_mode) {
          case REC_MODE_FULL: 
            looper_a.setRecModeFull(); 
            looper_b.setRecModeFull(); 
            looper_c.setRecModeFull(); 
            looper_d.setRecModeFull(); 
            looper_e.setRecModeFull(); 
            break;
          case REC_MODE_LOOP:
            looper_a.setRecModeLoop(); 
            looper_b.setRecModeLoop(); 
            looper_c.setRecModeLoop();
            looper_d.setRecModeLoop();
            looper_e.setRecModeLoop(); 
            break;
          case REC_MODE_FULL_SHOT:
            looper_a.setRecModeFullShot();
            looper_b.setRecModeFullShot();
            looper_c.setRecModeFullShot();
            looper_d.setRecModeFullShot();
            looper_e.setRecModeFullShot();
            break;
        };

        // Setup Button functions (these should enter the ButtonGrid Menus)
        button_1.onHold([this](){ this->setMode(UI_MODE_REC_MENU); });
        button_2.onHold([this](){ this->setMode(UI_MODE_PLAY_MENU); });
        button_3.onHold([this](){ this->setMode(UI_MODE_FX_MENU); });
        button_6.onHold([this](){ this->setMode(UI_MODE_BUFFER_MENU); });

        // Set the recording/overdub buttons to toggle or momentary
        // depending on the value of the option
        if (rec_button_momentary) {
          button_4.onHold([this](){ this->activateRecording(); });
          button_5.onHold([this](){ this->activateOverdub(); });
          button_4.onReleased([this](){ this->stopRecording(); });
          button_5.onReleased([this](){ this->stopRecording(); });
        } else {
          button_4.onReleased([this](){ this->toggleRecording(); });
          button_5.onReleased([this](){ this->toggleOverdub(); });
        }
        
        // Store the last ui mode, for the check on top
        last_ui_mode = ui_mode;
      }
    }

    // Render the default screen (waveform)
    void renderDefault() {
      // Store the current time and check how long ago the last frame was
      // in ms
      
      // Clear the display
      display.clearDisplay();

      // Waveform should be maximum screen-heigh
      int wave_height = display.height() * 1.0f;
      // Ensure that when stepping from left to right we fit the waveform on the screen
      int step = activeLooper()->getBufferLength() / (display.width() * WAVEFORM_OVERSAMPLING);
      // Helper variable for the bottom of the screen
      int bottom = display.height()-1;

      // Render the waveform by iterating through the samples (oversampled by a factor
      // defined on top of this file). Average the samples for each pixel of the 128 px
      // wide screen and cache the resulting heights so we only have to recalculate when
      // the waveform changes
      for (int i=0; i<display.width()*WAVEFORM_OVERSAMPLING; i+=WAVEFORM_OVERSAMPLING) {
        uint16_t x = int(i / WAVEFORM_OVERSAMPLING);
        // Only recalculate if the cahce is dirty, else use cache
        if (waveform_cache_dirty) {
          float sig = 0.0f;
          float scale = 1.0f;
          if (!WAVEFORM_LIN) {
            scale = 10.0f;
          }
          // Step through the buffer and sum the absolute values
          for (int s=0; s<WAVEFORM_OVERSAMPLING; s++) {
            float abs_sig = activeLooper()->getBuffer()[step*i];
            abs_sig = abs(abs_sig) * scale;
            sig += abs_sig;
          }
          // We oversampled so divide here
          sig = sig / float(WAVEFORM_OVERSAMPLING);

          if (!WAVEFORM_LIN) {
            // Volume is logarithmic (hiding silent noises)
            if (sig != 0.0f) {
              sig = log10(sig);
            }
          }
          waveform_cache[x] = int(sig * wave_height);
        }

        // Draw the vertical lines from bottom up, depending on the level of the
        // calulcated wave on this point of the screen
        display.drawFastVLine(x, bottom, -waveform_cache[x], SH110X_WHITE);
        
      }
      // Draw one horizontal line on bottom
      display.drawFastHLine(0, bottom, display.width(), SH110X_WHITE);

      // Cache is now marked as clean
      waveform_cache_dirty = false;

      // Draw Indicator for loop start 
      int x_start_loop = int(activeLooper()->loop_start_f * display.width());
      display.drawLine(x_start_loop, 0, x_start_loop, bottom, SH110X_WHITE);
      display.fillTriangle(x_start_loop, 6, x_start_loop, 0, x_start_loop+3, 0, SH110X_WHITE);

      // Draw Indicator for Loop End
      int x_loop_length = int(activeLooper()->loop_length_f * display.width());
      int x_loop_end = (x_start_loop + x_loop_length) % display.width();
      display.drawLine(x_loop_end, 0, x_loop_end, bottom, SH110X_WHITE);
      display.fillTriangle(x_loop_end, 6, x_loop_end-3, 0, x_loop_end, 0, SH110X_WHITE);

      // Draw connecting line for start and end
      if (x_loop_end >= x_start_loop) {
        display.drawLine(x_start_loop, 0, x_loop_end, 0, SH110X_WHITE);
      } else {
        display.drawLine(x_start_loop, 0, display.width(), 0, SH110X_WHITE);
        display.drawLine(0, 0, x_loop_end, 0, SH110X_WHITE);
      }

      // Draw Playhead
      switch (activeLooper()->playback_state) {
        case atoav::PLAYBACK_STATE_LOOP:
          {
            int x_playhead = int(activeLooper()->GetPlayhead() * display.width()) + x_start_loop;
            display.drawFastVLine(x_playhead, 6, 24, SH110X_WHITE);
            break;
          }
        case atoav::PLAYBACK_STATE_MULTILOOP:
          {
            float* playheads = activeLooper()->GetPlayheads();
            uint8_t count = activeLooper()->GetPlayheadCount();
            int x_playhead = 0;
            for (size_t i=0; i<count; i++) {
              x_playhead = int(playheads[i] * display.width()) + x_start_loop;
              int h = 6 + i*3;
              display.drawFastVLine(x_playhead, h, 3, SH110X_WHITE);
            }
            break;
          }
        case atoav::PLAYBACK_STATE_MIDI:
          {
            int x_playhead = int(activeLooper()->GetPlayhead() * display.width()) + x_start_loop;
            display.drawFastVLine(x_playhead, 6, 24, SH110X_WHITE);
            break;
          }
      }
      

      // Draw Recording Indicator and Recording Head
      if (recording_state == REC_STATE_RECORDING) {
        // Draw Rec Head
        int x_rec_head = int(activeLooper()->GetRecHead() * display.width());
        display.drawFastVLine(x_rec_head, 10, bottom, SH110X_WHITE);
        display.fillCircle(x_rec_head, 10, 3, SH110X_WHITE);
        // Record sign
        display.fillRect(0, 0, 13, 13, SH110X_WHITE);
        display.fillRect(2, 2, 12, 12, SH110X_WHITE);
        display.fillRect(1, 1, 11, 11, SH110X_BLACK);
        display.fillCircle(6, 6, 3, SH110X_WHITE);
      }

      // Draw Overdub Indicator and Recording Head
      if (recording_state == REC_STATE_OVERDUBBING) {
        // Draw Rec Head
        int x_rec_head = int(activeLooper()->GetRecHead() * display.width());
        display.drawFastVLine(x_rec_head, 10, bottom, SH110X_WHITE);
        display.fillCircle(x_rec_head, 10, 3, SH110X_WHITE);

        // Overdub sign (a "plus")
        display.fillRect(0, 0, 13, 13, SH110X_WHITE);
        display.fillRect(2, 2, 12, 12, SH110X_WHITE);
        display.fillRect(1, 1, 11, 11, SH110X_BLACK);
        display.drawLine(6, 2, 6, 10, SH110X_WHITE);
        display.drawLine(2, 6, 10, 6, SH110X_WHITE);
      }

      // Render potentiometer UIs in case a knob is changed
      pot_1.renderUi();
      pot_2.renderUi();
      pot_3.renderUi();
      pot_4.renderUi();
      pot_5.renderUi();
      pot_6.renderUi();
      pot_7.renderUi();

      // Display all the things done above
      display.display();
    }

    // Activate recording and set the waveform cache to dirty
    void activateRecording() {
      if (recording_state != REC_STATE_RECORDING) {
        activeLooper()->SetRecord();
        waveform_cache_dirty = true;
        recording_state = REC_STATE_RECORDING;
      }
    }

    // Toggle recording
    void toggleRecording() {
      switch (recording_state) {
        case REC_STATE_NOT_RECORDING:
          activateRecording();
          break;
        case REC_STATE_RECORDING:
          stopRecording();
          break;
        case REC_STATE_OVERDUBBING:
          activateRecording();
          break;
      }
    }

    // Activates overdubbing
    void activateOverdub() {
      if (recording_state != REC_STATE_OVERDUBBING) {
        waveform_cache_dirty = true;
        recording_state = REC_STATE_OVERDUBBING;
        activeLooper()->SetOverdub();
      }
    }

    // Stop the recording
    void stopRecording() {
      if (recording_state != REC_STATE_NOT_RECORDING) {
        recording_state = REC_STATE_NOT_RECORDING;
        activeLooper()->SetStopWriting();
      }
    }

    // Toggle overdub off and on
    void toggleOverdub() {
      switch (recording_state) {
        case REC_STATE_NOT_RECORDING:
          activateOverdub();
          break;
        case REC_STATE_OVERDUBBING:
          stopRecording();
          break;
        case REC_STATE_RECORDING:
          activateOverdub();
          break;
      }
    }

    // Reset the recording state (mark waveform cahce dirty)
    void resetRecordingState() {
      if (recording_state == REC_STATE_RECORDING || recording_state == REC_STATE_OVERDUBBING) {
        waveform_cache_dirty = true; 
      }
    }

   // Set the mode of the UI (and thus change the screen)
    void setMode(UiMode mode) {
      if (last_ui_mode == mode) { return; }
      last_ui_mode = ui_mode;
      ui_mode = mode;
      switch (ui_mode) {
        case UI_MODE_SPLASH:
          break;
        case UI_MODE_DEFAULT:
          break;
        case UI_MODE_REC_MENU:
          this->button_grids[0].hideAllDescriptions();
          break;
        case UI_MODE_PLAY_MENU:
          this->button_grids[1].hideAllDescriptions();
          break;
        case UI_MODE_TRIGGER_MENU:
          this->button_grids[2].hideAllDescriptions();
          break;
        case UI_MODE_FX_MENU:
          this->button_grids[3].hideAllDescriptions();
          break;
        case UI_MODE_BUFFER_MENU:
          this->button_grids[4].hideAllDescriptions();
          break;
      }
    }


    // Update the Ui variables (expected to run repeatedly)
    void update() {
      resetRecordingState();

      switch (ui_mode) {
        case UI_MODE_SPLASH:
          break;
        case UI_MODE_DEFAULT:
          setupDefault();
          break;
        case UI_MODE_REC_MENU:
          setupRecMenu();
          break;
        case UI_MODE_PLAY_MENU:
          setupPlayMenu();
          break;
        case UI_MODE_TRIGGER_MENU:
          break;
        case UI_MODE_FX_MENU:
          setupFXMenu();
          break;
        case UI_MODE_BUFFER_MENU:
          setupBufferMenu();
          break;
      }
    }

    // Returns a pointer to the currently active looper
    atoav::Looper * activeLooper() {
      switch(active_buffer) {
        case ACTIVE_BUFFER_A: {
          atoav::Looper * ptr = &looper_a;
          return ptr;
          break;
        }
        case ACTIVE_BUFFER_B: {
          atoav::Looper * ptr = &looper_b;
          return ptr;
          break;
        }
        case ACTIVE_BUFFER_C: {
          atoav::Looper * ptr = &looper_c;
          return ptr;
          break;
        }
        case ACTIVE_BUFFER_D: {
          atoav::Looper * ptr = &looper_d;
          return ptr;
          break;
        }
        case ACTIVE_BUFFER_E: {
          atoav::Looper * ptr = &looper_e;
          return ptr;
          break;
        }
      }
      // Unreachable, but makes the compiler shut up
      atoav::Looper * ptr = &looper_a;
      return ptr;
    }

    // Set the Looper start/length to a given value
    void setLoop(float start, float length) {
      activeLooper()->SetLoop(start, length);
    }

    private:
      double last_render = 0.0;
      uint16_t waveform_cache[128] = {0};
      bool waveform_cache_dirty = true;
      RecordingState recording_state = REC_STATE_NOT_RECORDING;
      UiMode last_ui_mode = UI_MODE_LAST;
      bool rec_button_momentary = true;
};



#endif