#pragma once

#include "luts.h"

namespace atoav {

enum RecPitchMode {
  REC_PITCH_MODE_NORMAL,
  REC_PITCH_MODE_PITCHED,
  REC_PITCH_MODE_UNPITCHED,
  REC_PITCH_MODE_LAST,
};

enum RecStartMode {
  REC_START_MODE_BUFFER,
  REC_START_MODE_LOOP,
  REC_START_MODE_PLAYHEAD,
  REC_START_MODE_LAST,
};

enum PlaybackState {
  PLAYBACK_STATE_STOPPED,
  PLAYBACK_STATE_LOOP,
  PLAYBACK_STATE_MULTILOOP,
  PLAYBACK_STATE_MIDI,
  PLAYBACK_STATE_LAST,
};

enum RecordingState {
  REC_STATE_EMPTY,
  REC_STATE_RECORDING,
  REC_STATE_OVERDUBBING,
  REC_STATE_ERASING,
  REC_STATE_NONE,
  REC_STATE_LAST,
};

// =================================================
// =                   H E A D                     =
// =================================================

class Head {
  public:
    Head();
    void activate();
    void deactivate();
    bool isActive();
    void setPosition(float value);
    void setIncrement(float value);
    void incrementBy(float value);
    void update();
    float read();
    float increment = 1.0f;
    float variation = 0.0f;
    float variation_amount = 0.0f;
  private:
    bool active = true;
    float position = 0.0f;
};

Head::Head() {
  variation = random();
}
void Head::activate() {
  this->active = true;
}
void Head::deactivate() {
  this->active = false;
}
bool Head::isActive() {
  return active;
}
void Head::setPosition(float value) {
  this->position = value;
}
void Head::setIncrement(float value) {
  this->increment = value;
}
void Head::incrementBy(float value) {
  this->position += value;
}
void Head::update() {
  this->position += this->increment + (variation * variation_amount);
}
float Head::read() {
  return this->position;
}


// =================================================
// =                 L O O P E R                   =
// =================================================

class Looper {
	public:
		Looper();
    void Init(float *buf, size_t buf_size);
    void SetRecord();
    void SetOverdub();
    void SetErase();
    void SetStopWriting();
    bool isWriting();
    void ResetRecHead();
    void SetLoop(float loop_start_time, float loop_length_time);
    void Record(float in);
    float Process();
    float* getBuffer();
    size_t getBufferLength();
    void setRecPitchMode(RecPitchMode mode);
    void setRecStartMode(RecStartMode mode);
    float GetPlayhead();
    float* GetPlayheads();
    uint8_t GetPlayheadCount();
    float GetRecHead();
    bool toggleRecMode();
    void setRecModeFull();
    void setRecModeLoop();
    void setRecModeFullShot();
    void setRecModeLoopShot();
    void setPlaybackSpeed(float increment);
    void addToPlayhead(float value);
    float loop_start_f = 0.0f;
    float loop_length_f = 1.0f;
    uint8_t grain_count = 9;
    float grain_spread = 1.0f;
    float grain_variation = 0.0f;
    RecPitchMode rec_pitch_mode = REC_PITCH_MODE_NORMAL;
    RecStartMode rec_start_mode = REC_START_MODE_BUFFER;
    PlaybackState playback_state = PLAYBACK_STATE_LOOP;
    RecordingState recording_state = REC_STATE_EMPTY;

  private:
    static const size_t kFadeLength = 200;
    static const size_t kMinLoopLength = 2 * kFadeLength;

    float* buffer;
    size_t buffer_length = 0;

    Head playheads[9];
    Head rec_head;

    size_t loop_start = 0;
    size_t loop_length = 48000;

    bool stop_after_recording = false;
    bool stay_within_loop = true;

};


Looper::Looper() {}

void Looper::Init(float *buf, size_t buf_size) {
  buffer = buf;
  buffer_length = buf_size;
  memset(buffer, 0, sizeof(float) * buffer_length);
}

void Looper::SetRecord() {
  recording_state = REC_STATE_RECORDING;
  ResetRecHead();
  rec_head.activate();
}

void Looper::SetOverdub() {
  recording_state = REC_STATE_OVERDUBBING;
  ResetRecHead();
  rec_head.activate();
}

void Looper::SetErase() {
  recording_state = REC_STATE_ERASING;
  ResetRecHead();
  rec_head.activate();
}

void Looper::SetStopWriting() {
  recording_state = REC_STATE_NONE;
  rec_head.deactivate();
}

bool Looper::isWriting() {
  return recording_state == REC_STATE_RECORDING 
    || recording_state == REC_STATE_OVERDUBBING 
    || recording_state == REC_STATE_ERASING; 
}

void Looper::ResetRecHead() {
  if (isWriting()) {
    switch (rec_start_mode) {
      case REC_START_MODE_LOOP:
        rec_head.setPosition(loop_start % buffer_length);
        break;
      case REC_START_MODE_BUFFER:
        rec_head.setPosition(0.0f);
        break;
      case REC_START_MODE_PLAYHEAD:
        rec_head.setPosition(fmod(loop_start + playheads[0].read(), float(buffer_length)));
        break;
    }
  }
}

void Looper::SetLoop(float loop_start_time, float loop_length_time) {
  if (!isnan(loop_start_time)) {
    loop_start_f = loop_start_time;
    loop_start = static_cast<size_t>(loop_start_time * (buffer_length - 1));
  }
  if (!isnan(loop_length_time)) {
    loop_length_f = loop_length_time;
    loop_length = max(kMinLoopLength, static_cast<size_t>(loop_length_time * buffer_length));
  }
}

void Looper::Record(float in) {
  // Overwrite/Add/Erase the buffer depending on the mode
  switch (recording_state) {
    case REC_STATE_RECORDING:
      buffer[int(rec_head.read())] = in;
      break;
    case REC_STATE_OVERDUBBING:
      buffer[int(rec_head.read())] += in;
      break;
    case REC_STATE_ERASING:
      buffer[int(rec_head.read())] = 0.0f;
      break;
  }

  // Advance the recording head if needed
  if (isWriting()) {
    // Set Recording head increment depending on the mode
    switch (rec_pitch_mode) {
      case REC_PITCH_MODE_NORMAL: 
        rec_head.setIncrement(1.0f); 
        break;
      case REC_PITCH_MODE_UNPITCHED: 
        rec_head.setIncrement(playheads[0].increment); 
        break;
      case REC_PITCH_MODE_PITCHED:
        if (playheads[0].increment != 0.0) {
          rec_head.setIncrement(1.0f/playheads[0].increment);
        }
        break;
    }
    // Increment recording head
    rec_head.update();

    // Limit the position of the rec head depending on the active mode
    if (!stop_after_recording) {
      if (!stay_within_loop) {
        // record into whole buffer
        rec_head.setPosition(fmod(rec_head.read(), float(buffer_length)));
      } else {
        // Limit rec head to stay inside the loop
        rec_head.setPosition(fmod(rec_head.read(), float(loop_start + loop_length)));
        rec_head.setPosition(max(float(loop_start), rec_head.read()));
      }
    } else {
      // Stop at end (either end of buffer or end of loop)
      if (!stay_within_loop) {
        if (rec_head.read() > buffer_length) { SetStopWriting(); }
      } else {
        if (rec_head.read() > loop_start + loop_length) { SetStopWriting(); }
      }
    }

    // Ensure the Rec-Head is never without bounds, even when running backwards
    if (rec_head.read() > buffer_length) {
      rec_head.setPosition(0.0f);
    } else if (rec_head.read() < 0) {
      rec_head.setPosition(buffer_length);
    }
  }
}

float Looper::Process() {
  // Early return if buffer is empty or not playing to save performance
  if (recording_state == REC_STATE_EMPTY 
    || playback_state == PLAYBACK_STATE_STOPPED) {
    return 0;
  }

  // Deactivate all playheads except first if playback state is Loop
  switch (playback_state) {
    case PLAYBACK_STATE_LOOP:
      playheads[0].activate();
      for (size_t i=1; i<9; i++) {
        playheads[i].deactivate();
      }
      break;
    case PLAYBACK_STATE_MULTILOOP:
      for (size_t i=0; i<9; i++) {
        playheads[i].activate();
      }
      break;
    case PLAYBACK_STATE_STOPPED:
      for (size_t i=0; i<9; i++) {
        playheads[i].deactivate();
      }
      break;
  }

  double mix = 0.0;

  for (size_t i=0; i<grain_count-1; i++) {
    // Skip inactive playheads
    if (!playheads[i].isActive()) continue;

    // Ensure we are actually inside the buffer
    int play_pos = int(loop_start + playheads[i].read()) % buffer_length;

    // Read from the buffer
    mix += buffer[play_pos];

    // Advance the playhead
    playheads[i].update();

    // Ensure the playhead stays within bounds of the loop
    if ((playheads[i].read()) >= loop_length) {
        playheads[i].setPosition(0.0f);
      } else if (playheads[i].read() <= 0.0f) {
        playheads[i].setPosition(loop_length);
      }
  }
  return saturate(mix);
}

float Looper::GetPlayhead() {
  return float(playheads[0].read()) / float(buffer_length);
}

float* Looper::GetPlayheads() {
  static float playhead_positions[9];
  for (size_t i=0; i<9; i++) {
    playhead_positions[i] = float(playheads[i].read()) / float(buffer_length);
  }
  return playhead_positions;
}

uint8_t Looper::GetPlayheadCount() {
  return grain_count;
}

float Looper::GetRecHead() {
  return  float(rec_head.read()) / float(buffer_length);
}

bool Looper::toggleRecMode() {
  stay_within_loop = !stay_within_loop;
  return stay_within_loop;
}

void Looper::setRecModeFull() {
  stay_within_loop = false;
  stop_after_recording = false;
}

void Looper::setRecModeLoop() {
  stay_within_loop = true;
  stop_after_recording = false;
}

void Looper::setRecModeFullShot() {
  stay_within_loop = false;
  stop_after_recording = true;
}

void Looper::setRecModeLoopShot() {
  stay_within_loop = true;
  stop_after_recording = true;
}

void Looper::setPlaybackSpeed(float increment) {
  switch (playback_state) {
    case PLAYBACK_STATE_LOOP:
      playheads[0].setIncrement(increment);
      break;
    case PLAYBACK_STATE_MULTILOOP:
      playheads[0].setIncrement(increment);
      for (size_t i=1; i<9; i++) {
        playheads[i].variation_amount = grain_variation;
        playheads[i].setIncrement(increment + increment*grain_spread);
      }
      break;
  }
}

void Looper::addToPlayhead(float value) {
  switch (playback_state) {
    case PLAYBACK_STATE_LOOP:
      playheads[0].incrementBy(value);
      break;
    case PLAYBACK_STATE_MULTILOOP:
      playheads[0].incrementBy(value);
      for (size_t i=1; i<9; i++) {
        playheads[i].incrementBy(value + value/(1+i));
      }
      break;
  }
}

float* Looper::getBuffer() {
  return buffer;
}

size_t Looper::getBufferLength() {
  return buffer_length;
}

void Looper::setRecPitchMode(RecPitchMode mode) {
  rec_pitch_mode = mode;
}

void Looper::setRecStartMode(RecStartMode mode) {
  rec_start_mode = mode;
}



}; // namespace atoav