#define AUTO_GAIN 0       // Auto volume adjustment (disabled for manual control)
#define VOL_THR 25        // Silence threshold (no display on matrix below this)
#define LOW_PASS 20       // Lower sensitivity threshold for noise (no jumps when no sound)
#define DEF_GAIN 80       // Default maximum threshold (ignored when GAIN_CONTROL is active)
#define FHT_N 256         // Spectrum width x2
#define LOG_OUT 1
#define PEAK_HOLD_TIME 2000     // Peak hold time in ms

// Button pins
#define BUTTON1 8
#define BUTTON2 9  
#define BUTTON3 10

// Manually defined array of tones, first smooth, then steeper
byte posOffset[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; // 1500 Hz
//byte posOffset[16] = {1, 2, 3, 4, 6, 8, 10, 13, 16, 20, 25, 30, 35, 40, 45, 50}; // 4000 Hz

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

#include <Wire.h>
#include <U8glib.h>  // http://rcl-radio.ru/wp-content/uploads/2023/04/U8glib.zip
#include <FHT.h>     // http://forum.rcl-radio.ru/misc.php?action=pan_download&item=297&download=1

#define EN 6
#define RW 5
#define CS 4

//U8GLIB_SH1106_128X64 lcd(U8G_I2C_OPT_DEV_0|U8G_I2C_OPT_FAST);  // Dev 0, Fast I2C / TWI
U8GLIB_ST7920_128X64_1X lcd(EN, RW, CS); // serial use, PSB = GND

byte gain = DEF_GAIN;   
unsigned long gainTimer, times;
byte maxValue, maxValue_f;
float k = 0.1;
byte ur[16], urr[16];

// Button state variables
bool button1State = false;
bool button2State = false;
bool button3State = false;
unsigned long button1Time = 0;
unsigned long button2Time = 0;
unsigned long button3Time = 0;

// Mode variables
byte displayMode = 0;           // 0=normal, 1=peak hold, 2=falling dots, 3=symmetrical
byte speedMode = 0;             // 0=normal, 1=fast, 2=slow
byte sensitivityMode = 0;       // 0=normal, 1=high, 2=low
byte peakHold[16];              // Peak hold values for each band
unsigned long peakTimer[16];    // Timer for peak decay

void setup() {
  delay(100); 
  sbi(ADCSRA, ADPS2);
  cbi(ADCSRA, ADPS1);
  sbi(ADCSRA, ADPS0);
  Serial.begin(9600);
  Wire.begin(); 
  Wire.setClock(800000L);
  lcd.begin();
  // lcd.setRot180();
  lcd.setFont(u8g_font_profont11r);
  analogReadResolution(10); // ADC 10 BIT
  analogReference(INTERNAL1V024);
  pinMode(A0, INPUT); // INPUT AUDIO
  
  // Initialize button pins
  pinMode(BUTTON1, INPUT_PULLUP);
  pinMode(BUTTON2, INPUT_PULLUP);
  pinMode(BUTTON3, INPUT_PULLUP);
  
  // Initialize peak hold array
  for(int i = 0; i < 16; i++) {
    peakHold[i] = 0;
    peakTimer[i] = 0;
  }
}

void handleButtons() {
  // Button 1 - Display Mode Cycle
  if (digitalRead(BUTTON1) == LOW) {
    if (millis() - button1Time > 300) { // Debounce
      displayMode = (displayMode + 1) % 4; // Cycle through 4 modes
      button1Time = millis();
    }
  }
  
  // Button 2 - Speed Mode Cycle  
  if (digitalRead(BUTTON2) == LOW) {
    if (millis() - button2Time > 300) {
      speedMode = (speedMode + 1) % 3; // Cycle through 3 speed modes
      button2Time = millis();
    }
  }
  
  // Button 3 - Sensitivity Cycle
  if (digitalRead(BUTTON3) == LOW) {
    if (millis() - button3Time > 300) {
      sensitivityMode = (sensitivityMode + 1) % 3; // Cycle through 3 sensitivity modes
      button3Time = millis();
      
      // Adjust gain based on sensitivity
      switch(sensitivityMode) {
        case 0: gain = DEF_GAIN; break;    // Normal
        case 1: gain = DEF_GAIN / 2; break; // High sensitivity
        case 2: gain = DEF_GAIN * 2; break; // Low sensitivity
      }
    }
  }
}

void updatePeakHold() {
  for (int i = 0; i < 16; i++) {
    int posLevel = map(fht_log_out[posOffset[i]], LOW_PASS, gain, 0, 60);
    posLevel = constrain(posLevel, 0, 60);
    
    if (posLevel > peakHold[i]) {
      peakHold[i] = posLevel;
      peakTimer[i] = millis();
    } else if (millis() - peakTimer[i] > PEAK_HOLD_TIME) {
      if (peakHold[i] > 0) peakHold[i]--;
    }
  }
}

void drawModeIndicators() {
  // Display mode indicators at top right
  lcd.setFont(u8g_font_04b_03);
  
  // Display mode indicator (N, P, D, S)
  char modeChar = 'N';
  switch(displayMode) {
    case 0: modeChar = 'N'; break; // Normal
    case 1: modeChar = 'P'; break; // Peak
    case 2: modeChar = 'D'; break; // Dot
    case 3: modeChar = 'S'; break; // Symmetrical
  }
  
  // Speed mode indicator (N, F, S)
  char speedChar = 'N';
  switch(speedMode) {
    case 0: speedChar = 'N'; break; // Normal
    case 1: speedChar = 'F'; break; // Fast
    case 2: speedChar = 'S'; break; // Slow
  }
  
  // Sensitivity indicator (N, H, L)
  char sensChar = 'N';
  switch(sensitivityMode) {
    case 0: sensChar = 'N'; break; // Normal
    case 1: sensChar = 'H'; break; // High
    case 2: sensChar = 'L'; break; // Low
  }
  
  // Draw all three indicators at top right
  lcd.drawStr(100, 5, String(modeChar).c_str());
  lcd.drawStr(110, 5, String(speedChar).c_str());
  lcd.drawStr(120, 5, String(sensChar).c_str());
}

void drawSpectrum() {
  lcd.firstPage();  
  do {
    for (int pos = 0; pos < 128; pos += 8) {
      int band = pos / 8;
      int posLevel = map(fht_log_out[posOffset[band]], LOW_PASS, gain, 0, 60);
      posLevel = constrain(posLevel, 0, 60);
      
      if(millis() - times < 2000) { 
        posLevel = 60; // Startup animation
      }
      
      urr[band] = posLevel;
      
      // Apply speed mode to falling effect
      int fallSpeed = 1;
      switch(speedMode) {
        case 0: fallSpeed = 1; break; // Normal
        case 1: fallSpeed = 3; break; // Fast fall
        case 2: fallSpeed = 1; if(random(2) == 0) fallSpeed = 0; break; // Slow/random
      }
      
      if(urr[band] < ur[band]) {
        ur[band] = max(ur[band] - fallSpeed, 0);
      } else {
        ur[band] = posLevel;
      }
      
      delayMicroseconds(200);
      
      // Draw based on display mode
      switch(displayMode) {
        case 0: // Normal bars
          for (int v_pos = 0; v_pos < ur[band] + 4; v_pos += 4) {
            lcd.drawBox(pos, 61 - v_pos, 6, 2);
          }
          break;
          
        case 1: // Peak hold with bars
          for (int v_pos = 0; v_pos < ur[band] + 4; v_pos += 4) {
            lcd.drawBox(pos, 61 - v_pos, 6, 2);
          }
          // Draw peak dots
          if(peakHold[band] > 0) {
            lcd.drawBox(pos + 1, 61 - peakHold[band], 4, 1);
          }
          break;
          
        case 2: // Falling dots
          for (int v_pos = 0; v_pos < ur[band]; v_pos += 4) {
            lcd.drawBox(pos + 1, 61 - v_pos, 4, 1);
          }
          break;
          
        case 3: // Symmetrical mode
          for (int v_pos = 0; v_pos < ur[band] + 4; v_pos += 4) {
            lcd.drawBox(pos, 61 - v_pos, 6, 2);
            lcd.drawBox(pos, 3 + v_pos, 6, 2); // Mirror at top
          }
          break;
      }
    }
    
    // Draw mode indicators at top right
    drawModeIndicators();
    
  } while(lcd.nextPage());
}

void loop() {
  analyzeAudio();
  handleButtons();
  updatePeakHold();
  drawSpectrum();
  
  if (AUTO_GAIN) {
    maxValue_f = maxValue * k + maxValue_f * (1 - k);
    if (millis() - gainTimer > 1500) {
      if (maxValue_f > VOL_THR) gain = maxValue_f;
      else gain = 100;
      gainTimer = millis();
    }
  }
}

void analyzeAudio() {
  for (int i = 0 ; i < FHT_N ; i++) {
    int sample = analogRead(A0);
    fht_input[i] = sample; // put real data into bins
  }
  fht_window();  // window the data for better frequency response
  fht_reorder(); // reorder the data before doing the fht
  fht_run();     // process the data in the fht
  fht_mag_log(); // take the output of the fht
}