#include "fl/audio/detector/chord.h"
#include "fl/audio/audio_context.h"
#include "fl/audio/fft/fft.h"
#include "fl/math/math.h"
#include "fl/log/log.h"
#include "fl/stl/noexcept.h"

namespace fl {
namespace audio {
namespace detector {

// Chord templates (intervals relative to root)
// Major: root, major third (4 semitones), perfect fifth (7 semitones)
// Minor: root, minor third (3 semitones), perfect fifth (7 semitones)
// etc.

struct ChordTemplate {
    ChordType type;
    int intervals[5];  // Relative semitone intervals, -1 = unused
    int numNotes;
};

static const ChordTemplate kChordTemplates[] = {
    {ChordType::MAJOR,      {0, 4, 7, -1, -1}, 3},
    {ChordType::MINOR,      {0, 3, 7, -1, -1}, 3},
    {ChordType::DIMINISHED, {0, 3, 6, -1, -1}, 3},
    {ChordType::AUGMENTED,  {0, 4, 8, -1, -1}, 3},
    {ChordType::MAJOR7,     {0, 4, 7, 11, -1}, 4},
    {ChordType::MINOR7,     {0, 3, 7, 10, -1}, 4},
    {ChordType::DOMINANT7,  {0, 4, 7, 10, -1}, 4},
    {ChordType::SUSPENDED2, {0, 2, 7, -1, -1}, 3},
    {ChordType::SUSPENDED4, {0, 5, 7, -1, -1}, 3},
};
static const int kNumChordTemplates = sizeof(kChordTemplates) / sizeof(ChordTemplate);

ChordDetector::ChordDetector()
    : mChordStartTime(0)
    , mChordEndTime(0)
    , mConfidenceThreshold(0.6f)
    , mMinChordDuration(200)  // 200ms minimum chord duration
{
    for (int i = 0; i < 12; i++) {
        mChroma[i] = 0.0f;
        mPrevChroma[i] = 0.0f;
    }

    // Pre-compute template lookups (O(1) access per frame)
    initializeTemplateMap();
}

ChordDetector::~ChordDetector() FL_NOEXCEPT = default;

void ChordDetector::initializeTemplateMap() {
    // Pre-compute lookup map from ChordType to ChordTemplate*
    // Avoids linear search through kChordTemplates (9 searches per frame)
    for (int i = 0; i < kNumChordTemplates; i++) {
        mTemplateMap[static_cast<int>(kChordTemplates[i].type)] = &kChordTemplates[i];
    }
}

void ChordDetector::update(shared_ptr<Context> context) {
    mRetainedFFT = context->getFFT(32);  // Higher resolution for pitch detection
    const fft::Bins& fft = *mRetainedFFT;
    u32 timestamp = context->getTimestamp();

    // Calculate chroma features
    calculateChroma(fft);

    // Detect current chord
    Chord detected = detectChord(mChroma, timestamp);

    // Check if chord changed
    if (detected.isValid() && detected.confidence >= mConfidenceThreshold) {
        if (!mCurrentChord.isValid() || !isSimilarChord(detected, mCurrentChord)) {
            // New chord detected
            if (mCurrentChord.isValid()) {
                mFireChordEnd = true;
            }

            mPreviousChord = mCurrentChord;
            mCurrentChord = detected;
            mChordStartTime = timestamp;

            mFireChordChange = true;

            FL_DBG("Chord detected: " << mCurrentChord.getRootName()
                   << mCurrentChord.getTypeName()
                   << " (conf: " << mCurrentChord.confidence << ")");
        } else {
            // Same chord, update confidence
            mCurrentChord.confidence = detected.confidence;
            mCurrentChord.timestamp = timestamp;
        }

        // Set flag for onChord callback every frame when chord is active
        mFireChord = true;
    } else {
        // No valid chord or low confidence
        if (mCurrentChord.isValid()) {
            u32 duration = timestamp - mChordStartTime;
            if (duration >= mMinChordDuration) {
                mChordEndTime = timestamp;
                mFireChordEnd = true;
            }
            mCurrentChord = Chord();  // Reset to invalid
        }
    }

    // Save chroma for next frame
    for (int i = 0; i < 12; i++) {
        mPrevChroma[i] = mChroma[i];
    }
}

void ChordDetector::fireCallbacks() {
    if (mFireChordEnd) {
        if (onChordEnd) onChordEnd();
        mFireChordEnd = false;
    }
    if (mFireChordChange) {
        if (onChordChange) onChordChange(mCurrentChord);
        mFireChordChange = false;
    }
    if (mFireChord) {
        if (onChord) onChord(mCurrentChord);
        mFireChord = false;
    }
}

void ChordDetector::reset() {
    mCurrentChord = Chord();
    mPreviousChord = Chord();
    mChordStartTime = 0;
    mChordEndTime = 0;
    for (int i = 0; i < 12; i++) {
        mChroma[i] = 0.0f;
        mPrevChroma[i] = 0.0f;
    }
}

void ChordDetector::calculateChroma(const fft::Bins& fft) {
    // Clear chroma
    for (int i = 0; i < 12; i++) {
        mChroma[i] = 0.0f;
    }

    // Map fft::FFT bins to pitch classes (chroma) using linearly-rebinned magnitudes.
    // Linear bins give evenly-spaced frequency mapping: freq = fmin + bin * binWidth.
    fl::span<const float> linearBins = fft.linear();
    const fl::size numBins = linearBins.size();
    const float fmin = fft.linearFmin();
    const float fmax = fft.linearFmax();
    const float linearBinWidth = (numBins > 0) ? (fmax - fmin) / static_cast<float>(numBins) : 1.0f;

    for (fl::size binIdx = 0; binIdx < numBins; binIdx++) {
        float magnitude = linearBins[binIdx];
        if (magnitude < 1e-6f) continue;

        // Calculate frequency for this linearly-spaced bin
        float freq = fmin + (static_cast<float>(binIdx) + 0.5f) * linearBinWidth;

        // Skip frequencies below 60Hz (too low for chord detection)
        if (freq < 60.0f) continue;

        // Convert frequency to MIDI note number (A4 = 440Hz = MIDI 69)
        float midiNote = 69.0f + 12.0f * (fl::logf(freq / 440.0f) / fl::logf(2.0f));

        // Get pitch class (0-11)
        int pitchClass = static_cast<int>(midiNote + 0.5f) % 12;
        if (pitchClass < 0) pitchClass += 12;

        // Accumulate magnitude into chroma
        mChroma[pitchClass] += magnitude;
    }

    // Normalize chroma
    normalizeChroma(mChroma);
}

Chord ChordDetector::detectChord(const float* chroma, u32 timestamp) {
    float bestScore = 0.0f;
    int bestRoot = -1;
    ChordType bestType = ChordType::UNKNOWN;

    // Try all root notes (0-11)
    for (int root = 0; root < 12; root++) {
        // Try all chord templates
        for (int t = 0; t < kNumChordTemplates; t++) {
            float score = matchChordPattern(chroma, root, kChordTemplates[t].type);
            if (score > bestScore) {
                bestScore = score;
                bestRoot = root;
                bestType = kChordTemplates[t].type;
            }
        }
    }

    // Confidence threshold
    if (bestScore < 0.3f) {
        return Chord();  // No valid chord
    }

    return Chord(bestRoot, bestType, bestScore, timestamp);
}

float ChordDetector::matchChordPattern(const float* chroma, int root, ChordType type) {
    // Find the matching template using pre-computed lookup (O(1) vs O(n) linear search)
    auto it = mTemplateMap.find(static_cast<int>(type));
    if (it == mTemplateMap.end()) return 0.0f;

    const ChordTemplate* tmpl = it->second;
    if (!tmpl) return 0.0f;

    // Calculate match score
    float matchScore = 0.0f;
    float totalChroma = 0.0f;

    // Sum chroma energy for chord notes
    for (int i = 0; i < tmpl->numNotes; i++) {
        int interval = tmpl->intervals[i];
        if (interval < 0) break;
        int pitchClass = (root + interval) % 12;
        matchScore += chroma[pitchClass];
    }

    // Sum total chroma energy
    for (int i = 0; i < 12; i++) {
        totalChroma += chroma[i];
    }

    // Penalize energy in non-chord notes
    float nonChordEnergy = totalChroma - matchScore;

    // Score is ratio of chord notes to total energy, minus non-chord penalty
    float score = 0.0f;
    if (totalChroma > 1e-6f) {
        score = matchScore / totalChroma;
        score -= 0.3f * (nonChordEnergy / totalChroma);
        score = fl::max(0.0f, score);
    }

    return score;
}

bool ChordDetector::isSimilarChord(const Chord& a, const Chord& b) {
    if (!a.isValid() || !b.isValid()) return false;

    // Same root and type = similar
    if (a.rootNote == b.rootNote && a.type == b.type) {
        return true;
    }

    // Check for enharmonic equivalents or closely related chords
    // For now, just exact match
    return false;
}

void ChordDetector::normalizeChroma(float* chroma) {
    // Find max value
    float maxVal = 0.0f;
    for (int i = 0; i < 12; i++) {
        if (chroma[i] > maxVal) {
            maxVal = chroma[i];
        }
    }

    // Normalize to 0-1 range
    if (maxVal > 1e-6f) {
        for (int i = 0; i < 12; i++) {
            chroma[i] /= maxVal;
        }
    }
}

float ChordDetector::chromaDistance(const float* a, const float* b) {
    float dist = 0.0f;
    for (int i = 0; i < 12; i++) {
        float diff = a[i] - b[i];
        dist += diff * diff;
    }
    return fl::sqrt(dist);
}

// Out-of-line definitions for Chord methods (needed for linking with unity builds)
const char* Chord::getRootName() const {
    static const char* noteNames[] = {
        "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
    };
    return (rootNote >= 0 && rootNote < 12) ? noteNames[rootNote] : "?";
}

const char* Chord::getTypeName() const {
    switch (type) {
        case ChordType::MAJOR: return "maj";
        case ChordType::MINOR: return "min";
        case ChordType::DIMINISHED: return "dim";
        case ChordType::AUGMENTED: return "aug";
        case ChordType::MAJOR7: return "maj7";
        case ChordType::MINOR7: return "min7";
        case ChordType::DOMINANT7: return "7";
        case ChordType::SUSPENDED2: return "sus2";
        case ChordType::SUSPENDED4: return "sus4";
        default: return "?";
    }
}

} // namespace detector
} // namespace audio
} // namespace fl
