/* Copyright 2013-2019 Matt Tytel
 *
 * vital 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.
 *
 * vital 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 vital.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "tuning.h"

#include "utils.h"

namespace {
  constexpr char kScalaFileExtension[] = ".scl";
  constexpr char kKeyboardMapExtension[] = ".kbm";
  constexpr char kTunFileExtension[] = ".tun";
  constexpr int kDefaultMidiReference = 60;
  constexpr char kScalaKbmComment = '!';
  constexpr char kTunComment = ';';

  enum ScalaReadingState {
    kDescription,
    kScaleLength,
    kScaleRatios
  };

  enum KbmPositions {
    kMapSizePosition,
    kStartMidiMapPosition,
    kEndMidiMapPosition,
    kMidiMapMiddlePosition,
    kReferenceNotePosition,
    kReferenceFrequencyPosition,
    kScaleDegreePosition,
  };

  enum TunReadingState {
    kScanningForSection,
    kTuning,
    kExactTuning
  };

  String extractFirstToken(const String& source) {
    StringArray tokens;
    tokens.addTokens(source, false);
    return tokens[0];
  }

  float readCentsToTranspose(const String& cents) {
    return cents.getFloatValue() / vital::kCentsPerNote;
  }

  float readRatioToTranspose(const String& ratio) {
    StringArray tokens;
    tokens.addTokens(ratio, "/", "");
    float value = tokens[0].getIntValue();

    if (tokens.size() == 2)
      value /= tokens[1].getIntValue();

    return vital::utils::ratioToMidiTranspose(value);
  }

  String readTunSection(const String& line) {
    return line.substring(1, line.length() - 1).toLowerCase();
  }

  bool isBaseFrequencyAssignment(const String& line) {
    return line.upToFirstOccurrenceOf("=", false, true).toLowerCase().trim() == "basefreq";
  }

  int getNoteAssignmentIndex(const String& line) {
    String variable = line.upToFirstOccurrenceOf("=", false, true);
    StringArray tokens;
    tokens.addTokens(variable, false);
    if (tokens.size() <= 1 || tokens[0].toLowerCase() != "note")
      return -1;
    int index = tokens[1].getIntValue();
    if (index < 0 || index >= vital::kMidiSize)
      return -1;
    return index;
  }

  float getAssignmentValue(const String& line) {
    String value = line.fromLastOccurrenceOf("=", false, true).trim();
    return value.getFloatValue();
  }
}

String Tuning::allFileExtensions() {
  return String("*") + kScalaFileExtension + String(";") +
         String("*") + kKeyboardMapExtension + String(";") +
         String("*") + kTunFileExtension;
}

int Tuning::noteToMidiKey(const String& note_text) {
  constexpr int kNotesInScale = 7;
  constexpr int kOctaveStart = -1;
  constexpr int kScale[kNotesInScale] = { -3, -1, 0, 2, 4, 5, 7 };

  String text = note_text.toLowerCase().removeCharacters(" ");
  if (note_text.length() < 2)
    return -1;

  char note_in_scale = text[0] - 'a';
  if (note_in_scale < 0 || note_in_scale >= kNotesInScale)
    return -1;

  int offset = kScale[note_in_scale];
  text = text.substring(1);
  if (text[0] == '#') {
    text = text.substring(1);
    offset++;
  }
  else if (text[0] == 'b') {
    text = text.substring(1);
    offset--;
  }

  if (text.length() == 0)
    return -1;

  bool negative = false;
  if (text[0] == '-') {
    text = text.substring(1);
    negative = true;
    if (text.length() == 0)
      return -1;
  }
  int octave = text[0] - '0';
  if (negative)
    octave = -octave;
  octave = octave - kOctaveStart;
  return vital::kNotesPerOctave * octave + offset;
}

Tuning Tuning::getTuningForFile(File file) {
  return Tuning(file);
}

void Tuning::loadFile(File file) {
  String extension = file.getFileExtension().toLowerCase();
  if (extension == String(kScalaFileExtension))
    loadScalaFile(file);
  else if (extension == String(kTunFileExtension))
    loadTunFile(file);
  else if (extension == String(kKeyboardMapExtension))
    loadKeyboardMapFile(file);

  default_ = false;
}

void Tuning::loadScalaFile(const StringArray& scala_lines) {
  ScalaReadingState state = kDescription;

  int scale_length = 1;
  std::vector<float> scale;
  scale.push_back(0.0f);

  for (const String& line : scala_lines) {
    String trimmed_line = line.trim();
    if (trimmed_line.length() > 0 && trimmed_line[0] == kScalaKbmComment)
      continue;

    if (scale.size() >= scale_length + 1)
      break;

    switch (state) {
      case kDescription:
        state = kScaleLength;
        break;
      case kScaleLength:
        scale_length = extractFirstToken(trimmed_line).getIntValue();
        state = kScaleRatios;
        break;
      case kScaleRatios: {
        String tuning = extractFirstToken(trimmed_line);
        if (tuning.contains("."))
          scale.push_back(readCentsToTranspose(tuning));
        else
          scale.push_back(readRatioToTranspose(tuning));
        break;
      }
    }
  }

  keyboard_mapping_.clear();
  for (int i = 0; i < scale.size() - 1; ++i)
    keyboard_mapping_.push_back(i);
  scale_start_midi_note_ = kDefaultMidiReference;
  reference_midi_note_ = 0;

  loadScale(scale);
  default_ = false;
}

void Tuning::loadScalaFile(File scala_file) {
  StringArray lines;
  scala_file.readLines(lines);
  loadScalaFile(lines);
  tuning_name_ = scala_file.getFileNameWithoutExtension().toStdString();
}

void Tuning::loadKeyboardMapFile(File kbm_file) {
  static constexpr int kHeaderSize = 7;

  StringArray lines;
  kbm_file.readLines(lines);

  float header_data[kHeaderSize];
  memset(header_data, 0, kHeaderSize * sizeof(float));
  int header_position = 0;
  int map_size = 0;
  int last_scale_value = 0;
  keyboard_mapping_.clear();

  for (const String& line : lines) {
    String trimmed_line = line.trim();
    if (trimmed_line.length() > 0 && trimmed_line[0] == kScalaKbmComment)
      continue;

    if (header_position >= kHeaderSize) {
      String token = extractFirstToken(trimmed_line);
      if (token.toLowerCase()[0] != 'x')
        last_scale_value = token.getIntValue();
     
      keyboard_mapping_.push_back(last_scale_value);

      if (keyboard_mapping_.size() >= map_size)
        break;
    }
    else {
      header_data[header_position] = extractFirstToken(trimmed_line).getFloatValue();
      if (header_position == kMapSizePosition)
        map_size = header_data[header_position];
      header_position++;
    }
  }

  setStartMidiNote(header_data[kMidiMapMiddlePosition]);
  setReferenceNoteFrequency(header_data[kReferenceNotePosition], header_data[kReferenceFrequencyPosition]);
  loadScale(scale_);

  mapping_name_ = kbm_file.getFileNameWithoutExtension().toStdString();
}

void Tuning::loadTunFile(File tun_file) {
  keyboard_mapping_.clear();

  TunReadingState state = kScanningForSection;
  StringArray lines;
  tun_file.readLines(lines);

  int last_read_note = 0;
  float base_frequency = vital::kMidi0Frequency;
  std::vector<float> scale;
  for (int i = 0; i < vital::kMidiSize; ++i)
    scale.push_back(i);

  for (const String& line : lines) {
    String trimmed_line = line.trim();
    if (trimmed_line.length() == 0 || trimmed_line[0] == kTunComment)
      continue;

    if (trimmed_line[0] == '[') {
      String section = readTunSection(trimmed_line);
      if (section == "tuning")
        state = kTuning;
      else if (section == "exact tuning")
        state = kExactTuning;
      else
        state = kScanningForSection;
    }
    else if (state == kTuning || state == kExactTuning) {
      if (isBaseFrequencyAssignment(trimmed_line))
        base_frequency = getAssignmentValue(trimmed_line);
      else {
        int index = getNoteAssignmentIndex(trimmed_line);
        last_read_note = std::max(last_read_note, index);
        if (index >= 0)
          scale[index] = getAssignmentValue(trimmed_line) / vital::kCentsPerNote;
      }
    }
  }

  scale.resize(last_read_note + 1);
  
  loadScale(scale);
  setStartMidiNote(0);
  setReferenceFrequency(base_frequency);
  tuning_name_ = tun_file.getFileNameWithoutExtension().toStdString();
}

Tuning::Tuning() : default_(true) {
  scale_start_midi_note_ = kDefaultMidiReference;
  reference_midi_note_ = 0;

  setDefaultTuning();
}

Tuning::Tuning(File file) : Tuning() {
  loadFile(file);
}

void Tuning::loadScale(std::vector<float> scale) {
  scale_ = scale;
  if (scale.size() <= 1) {
    setConstantTuning(kDefaultMidiReference);
    return;
  }

  int scale_size = static_cast<int>(scale.size() - 1);
  int mapping_size = scale_size;
  if (keyboard_mapping_.size())
    mapping_size = static_cast<int>(keyboard_mapping_.size());

  float octave_offset = scale[scale_size];
  int start_octave = -kTuningCenter / mapping_size - 1;
  int mapping_position = -kTuningCenter - start_octave * mapping_size;

  float current_offset = start_octave * octave_offset;
  for (int i = 0; i < kTuningSize; ++i) {
    if (mapping_position >= mapping_size) {
      current_offset += octave_offset;
      mapping_position = 0;
    }

    int note_in_scale = mapping_position;
    if (keyboard_mapping_.size())
      note_in_scale = keyboard_mapping_[mapping_position];

    tuning_[i] = current_offset + scale[note_in_scale];
    mapping_position++;
  }
}

void Tuning::setConstantTuning(float note) {
  for (int i = 0; i < kTuningSize; ++i)
    tuning_[i] = note;
}

void Tuning::setDefaultTuning() {
  for (int i = 0; i < kTuningSize; ++i)
    tuning_[i] = i - kTuningCenter;

  scale_.clear();
  for (int i = 0; i <= vital::kNotesPerOctave; ++i)
    scale_.push_back(i);

  keyboard_mapping_.clear();

  default_ = true;
  tuning_name_ = "";
  mapping_name_ = "";
}

vital::mono_float Tuning::convertMidiNote(int note) const {
  int scale_offset = note - scale_start_midi_note_;
  return tuning_[kTuningCenter + scale_offset] + scale_start_midi_note_ + reference_midi_note_;
}

void Tuning::setReferenceFrequency(float frequency) {
  setReferenceNoteFrequency(0, frequency);
}

void Tuning::setReferenceNoteFrequency(int midi_note, float frequency) {
  reference_midi_note_ = vital::utils::frequencyToMidiNote(frequency) - midi_note;
}

void Tuning::setReferenceRatio(float ratio) {
  reference_midi_note_ = vital::utils::ratioToMidiTranspose(ratio);
}

json Tuning::stateToJson() const {
  json data;
  data["scale_start_midi_note"] = scale_start_midi_note_;
  data["reference_midi_note"] = reference_midi_note_;
  data["tuning_name"] = tuning_name_;
  data["mapping_name"] = mapping_name_;
  data["default"] = default_;

  json scale_data;
  for (float scale_value : scale_)
    scale_data.push_back(scale_value);
  data["scale"] = scale_data;

  if (keyboard_mapping_.size()) {
    json mapping_data;
    for (int mapping_value : keyboard_mapping_)
      mapping_data.push_back(mapping_value);
    data["mapping"] = mapping_data;
  }

  return data;
}

void Tuning::jsonToState(const json& data) {
  scale_start_midi_note_ = data["scale_start_midi_note"];
  reference_midi_note_ = data["reference_midi_note"];
  std::string tuning_name = data["tuning_name"];
  tuning_name_ = tuning_name;
  std::string mapping_name = data["mapping_name"];
  mapping_name_ = mapping_name;
  
  if (data.count("default"))
    default_ = data["default"];

  json scale_data = data["scale"];
  scale_.clear();
  for (json& value : scale_data) {
    float scale_value = value;
    scale_.push_back(scale_value);
  }

  keyboard_mapping_.clear();
  if (data.count("mapping")) {
    json mapping_data = data["mapping"];
    for (json& value : mapping_data) {
      int keyboard_value = value;
      keyboard_mapping_.push_back(keyboard_value);
    }
  }

  loadScale(scale_);
}
