// note_entry.cpp — see note_entry.h. PURE text->MIDI-note parse for the S12 numeric entry.

#include "note_entry.h"

#include <algorithm>
#include <cctype>

namespace reasampler::vst {

namespace {
char asciiUpper(char c) {
    return static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}

std::string trim(const std::string& s) {
    std::size_t a = 0;
    std::size_t b = s.size();
    while (a < b && std::isspace(static_cast<unsigned char>(s[a]))) ++a;
    while (b > a && std::isspace(static_cast<unsigned char>(s[b - 1]))) --b;
    return s.substr(a, b - a);
}

int clampNote(long long n) {
    if (n < 0) return 0;
    if (n > 127) return 127;
    return static_cast<int>(n);
}

// Semitone offset within an octave for a note letter (C..B), or -1 for a non-letter.
int letterSemitone(char up) {
    switch (up) {
        case 'C': return 0;
        case 'D': return 2;
        case 'E': return 4;
        case 'F': return 5;
        case 'G': return 7;
        case 'A': return 9;
        case 'B': return 11;
        default: return -1;
    }
}

// Parse a note name like "C4", "F#3", "Bb-1" (case-insensitive). MIDI 0 == C-1, 60 == C4
// (the DAW convention the editor's noteLabel uses). Returns nullopt if it is not a note name.
std::optional<int> parseNoteName(const std::string& s) {
    if (s.empty()) return std::nullopt;
    std::size_t i = 0;
    const int base = letterSemitone(asciiUpper(s[i]));
    if (base < 0) return std::nullopt;  // not a letter -> not a note name
    ++i;
    int semitone = base;
    // Optional accidental(s): # / b (or 's'/'f' are NOT accepted — keep it to the two glyphs).
    while (i < s.size() && (s[i] == '#' || s[i] == 'b' || s[i] == 'B')) {
        // A trailing 'b'/'B' could be a flat OR the start of nothing; here after a letter it is
        // an accidental. '#' raises, 'b'/'B' lowers.
        if (s[i] == '#') ++semitone;
        else --semitone;
        ++i;
    }
    // The octave: an optional sign then digits, running to the end.
    if (i >= s.size()) return std::nullopt;  // a bare "C" has no octave -> reject (ambiguous)
    bool neg = false;
    if (s[i] == '+' || s[i] == '-') {
        neg = (s[i] == '-');
        ++i;
    }
    if (i >= s.size()) return std::nullopt;
    int octave = 0;
    bool anyDigit = false;
    for (; i < s.size(); ++i) {
        if (!std::isdigit(static_cast<unsigned char>(s[i]))) return std::nullopt;
        octave = octave * 10 + (s[i] - '0');
        anyDigit = true;
    }
    if (!anyDigit) return std::nullopt;
    if (neg) octave = -octave;
    // MIDI note = (octave + 1) * 12 + semitone  (C-1 == 0, C4 == 60).
    const long long note = static_cast<long long>(octave + 1) * 12 + semitone;
    return clampNote(note);
}

std::optional<int> parseInteger(const std::string& s) {
    if (s.empty()) return std::nullopt;
    std::size_t i = 0;
    bool neg = false;
    if (s[i] == '+' || s[i] == '-') {
        neg = (s[i] == '-');
        ++i;
    }
    if (i >= s.size()) return std::nullopt;
    long long v = 0;
    for (; i < s.size(); ++i) {
        if (!std::isdigit(static_cast<unsigned char>(s[i]))) return std::nullopt;
        v = v * 10 + (s[i] - '0');
        if (v > 1000000) v = 1000000;  // saturate; clampNote takes it to 127 anyway
    }
    if (neg) v = -v;
    return clampNote(v);
}
}  // namespace

std::optional<int> parseNoteEntry(const std::string& text) {
    const std::string s = trim(text);
    if (s.empty()) return std::nullopt;
    // Try a plain integer first (the common MIDI-number case); fall back to a note name.
    if (std::isdigit(static_cast<unsigned char>(s[0])) || s[0] == '+' ||
        (s[0] == '-' && s.size() > 1 && std::isdigit(static_cast<unsigned char>(s[1])))) {
        if (auto n = parseInteger(s)) return n;
    }
    return parseNoteName(s);
}

}  // namespace reasampler::vst
