#include <Arduino.h>
#include <DFMiniMp3.h>

class Mp3Notify;

typedef DFMiniMp3<HardwareSerial, Mp3Notify> DfMp3;
DfMp3 mp3(Serial2);

constexpr int kDfPlayerRxPin = 16;
constexpr int kDfPlayerTxPin = 17;
constexpr unsigned long kDfPlayerBaud = 9600;
constexpr uint32_t kDfPlayerOnlineTimeoutMs = 2000;

constexpr int kLedPins[] = {18, 19, 21, 22};
constexpr size_t kLedCount = sizeof(kLedPins) / sizeof(kLedPins[0]);

constexpr int kBuzzerPins[] = {33, 25, 26, 27};
constexpr size_t kBuzzerCount = sizeof(kBuzzerPins) / sizeof(kBuzzerPins[0]);
constexpr size_t kPairCount = (kLedCount < kBuzzerCount) ? kLedCount : kBuzzerCount;

constexpr int kResetPin = 23;
constexpr uint32_t kAdminHoldMillis = 3000;
constexpr uint32_t kResetReleaseDelayMs = 300;

int gActiveBuzzer = -1;
bool gAdminMode = false;
uint32_t gResetPressStartMs = 0;

void setLed(size_t index, bool on)
{
    if (index < kLedCount)
    {
        digitalWrite(kLedPins[index], on ? HIGH : LOW);
    }
}

void setAllLeds(bool on)
{
    for (int pin : kLedPins)
    {
        digitalWrite(pin, on ? HIGH : LOW);
    }
}

void syncLedsToButtons()
{
    for (size_t i = 0; i < kPairCount; ++i)
    {
        const bool pressed = (digitalRead(kBuzzerPins[i]) == LOW);
        setLed(i, pressed);
    }
    for (size_t i = kPairCount; i < kLedCount; ++i)
    {
        setLed(i, false);
    }
}

class Mp3Notify
{
public:
    static void PrintlnSourceAction(DfMp3_PlaySources source, const char *action)
    {
        if (source & DfMp3_PlaySources_Sd)
        {
            Serial.print("SD Card, ");
        }
        if (source & DfMp3_PlaySources_Usb)
        {
            Serial.print("USB Disk, ");
        }
        if (source & DfMp3_PlaySources_Flash)
        {
            Serial.print("Flash, ");
        }
        Serial.println(action);
    }
    static void OnError([[maybe_unused]] DfMp3 &mp3, uint16_t errorCode)
    {
        // see DfMp3_Error for code meaning
        Serial.println();
        Serial.print("Com Error ");
        Serial.println(errorCode);
    }
    static void OnPlayFinished([[maybe_unused]] DfMp3 &mp3, [[maybe_unused]] DfMp3_PlaySources source, uint16_t track)
    {
        Serial.print("Play finished for #");
        Serial.println(track);
        // No automatic restart; playback only occurs on the first buzzer press.
    }
    static void OnPlaySourceOnline([[maybe_unused]] DfMp3 &mp3, DfMp3_PlaySources source)
    {
        PrintlnSourceAction(source, "online");
    }
    static void OnPlaySourceInserted([[maybe_unused]] DfMp3 &mp3, DfMp3_PlaySources source)
    {
        PrintlnSourceAction(source, "inserted");
    }
    static void OnPlaySourceRemoved([[maybe_unused]] DfMp3 &mp3, DfMp3_PlaySources source)
    {
        PrintlnSourceAction(source, "removed");
    }
};

void setup()
{
    Serial.begin(9600);
    Serial.println("Booting...");

    for (int pin : kLedPins)
    {
        pinMode(pin, OUTPUT);
        digitalWrite(pin, LOW);
    }

    for (int pin : kBuzzerPins)
    {
        pinMode(pin, INPUT_PULLUP);
    }

    pinMode(kResetPin, INPUT_PULLUP);

    syncLedsToButtons();

    mp3.begin(kDfPlayerRxPin, kDfPlayerTxPin, kDfPlayerBaud);
    mp3.setComRetries(1);

    delay(200);
    mp3.reset(false);
    delay(1200);

    for (size_t i = 0; i < kPairCount; ++i)
    {
        if (kBuzzerPins[i] == kDfPlayerRxPin || kBuzzerPins[i] == kDfPlayerTxPin)
        {
            Serial.print("WARNING: buzzer pin conflicts with DF serial: index ");
            Serial.println(i);
        }
    }

    const uint32_t waitStart = millis();
    while (!mp3.isOnline() && (millis() - waitStart) < kDfPlayerOnlineTimeoutMs)
    {
        mp3.loop();
        delay(1);
    }

    const bool playerOnline = mp3.isOnline();
    if (playerOnline)
    {
        uint16_t version = mp3.getSoftwareVersion();
        Serial.print("version ");
        Serial.println(version);

        uint16_t volume = mp3.getVolume();
        Serial.print("volume ");
        Serial.println(volume);
        mp3.setVolume(24);

        const uint16_t count = mp3.getTotalTrackCount(DfMp3_PlaySource_Sd);
        Serial.print("files ");
        Serial.println(count);

        Serial.println("ready.");
    }
    else
    {
        Serial.println("DFPlayer did not come online.");
    }
}

void loop()
{
    mp3.loop();

    const bool resetPressed = (digitalRead(kResetPin) == LOW);
    if (resetPressed && gResetPressStartMs == 0)
    {
        gResetPressStartMs = millis();
        mp3.stop();
    }

    if (resetPressed && !gAdminMode && (millis() - gResetPressStartMs) > kAdminHoldMillis)
    {
        gAdminMode = true;
        setAllLeds(true);
    }

    if (!resetPressed && gResetPressStartMs != 0)
    {
        if (gAdminMode)
        {
            gAdminMode = false;
            setAllLeds(false);
            gActiveBuzzer = -1;
        }
        else if (gActiveBuzzer != -1)
        {
            setLed(static_cast<size_t>(gActiveBuzzer), false);
            gActiveBuzzer = -1;
        }
        gResetPressStartMs = 0;
        delay(kResetReleaseDelayMs);
    }

    if (gActiveBuzzer == -1 && !gAdminMode)
    {
        for (size_t i = 0; i < kPairCount; ++i)
        {
            if (digitalRead(kBuzzerPins[i]) == LOW)
            {
                gActiveBuzzer = static_cast<int>(i);
                for (size_t j = 0; j < kLedCount; ++j)
                {
                    setLed(j, (j == i));
                }
                mp3.playMp3FolderTrack(static_cast<uint16_t>(i + 1));
                break;
            }
        }
        if (gActiveBuzzer == -1)
        {
            syncLedsToButtons();
        }
    }
    else if (!gAdminMode && gActiveBuzzer >= 0)
    {
        setLed(static_cast<size_t>(gActiveBuzzer), true);
    }

    delay(1);
}
