#include "DirectoryNode.h"
#include "globals.h"
#include <algorithm>
#include <cstring>    // strlen, strlcpy, strlcat
#include <strings.h>  // strcasecmp

char DirectoryNode::buffer[DirectoryNode::buffer_size];

DirectoryNode::DirectoryNode(const String &nodeName)
    : name(nodeName), currentPlaying("")
{
    id = DirectoryNode::idCounter;
    DirectoryNode::idCounter++;
}

DirectoryNode::~DirectoryNode()
{
    for (DirectoryNode *childNode : subdirectories)
    {
        delete childNode;
    }
}

const uint16_t DirectoryNode::getId() const
{
    return id;
}

const String &DirectoryNode::getName() const
{
    return name;
}

const std::vector<DirectoryNode *> &DirectoryNode::getSubdirectories() const
{
    return subdirectories;
}

const std::vector<String> &DirectoryNode::getMP3Files() const
{
    return mp3Files;
}

const String &DirectoryNode::getDirPath() const
{
    return dirPath;
}

uint16_t DirectoryNode::getFileIdAt(size_t i) const
{
    return (i < ids.size()) ? ids[i] : 0;
}



String DirectoryNode::buildFullPath(const String &fileName) const
{
    if (dirPath == "/")
    {
        String p = "/";
        p += fileName;
        return p;
    }
    String p = dirPath;
    p += "/";
    p += fileName;
    return p;
}

bool DirectoryNode::comparePathWithString(const char* path, const String& target) const
{
    // Convert target to char* for comparison
    const char* targetStr = target.c_str();
    
    // Case-insensitive string comparison
    return strcasecmp(path, targetStr) == 0;
}

void DirectoryNode::buildFullPath(const String &fileName, char* out, size_t n) const
{
    if (n == 0) return;
    out[0] = '\0';

    if (dirPath == "/")
    {
        strlcat(out, "/", n);
    }
    else
    {
        strlcpy(out, dirPath.c_str(), n);
        strlcat(out, "/", n);
    }

    strlcat(out, fileName.c_str(), n);
}

void DirectoryNode::setCurrentPlaying(const String &mp3File)
{
    bool isAbs = (mp3File.length() > 0) && (mp3File.charAt(0) == '/');
    const String &fileName = isAbs ? mp3File.substring(mp3File.lastIndexOf('/') + 1) : mp3File;
    
    if (isAbs)
    {
        currentPlaying = mp3File;  // Already absolute path
    }
    else
    {
        // Use buffer for building relative path
        buildFullPath(fileName, buffer, buffer_size);
        currentPlaying = String(buffer);  // Convert back to String for assignment
    }
    
    for (size_t i = 0; i < mp3Files.size(); i++)
    {
        if (mp3Files[i] == fileName && ids.size() > i)
        {
            currentPlayingId = ids[i];
            break;
        }
    }
}

const String &DirectoryNode::getCurrentPlaying() const
{
    return currentPlaying;
}

const uint16_t DirectoryNode::getCurrentPlayingId() const
{
    return currentPlayingId;
}

uint16_t DirectoryNode::getNextId()
{
    uint16_t next = DirectoryNode::idCounter;
    DirectoryNode::idCounter++;
    return next;
}

void DirectoryNode::addSubdirectory(DirectoryNode *subdirectory)
{
    subdirectories.push_back(subdirectory);
}

void DirectoryNode::addMP3File(const String &mp3File)
{
    mp3Files.push_back(mp3File);
    ids.push_back(getNextId());
}

void DirectoryNode::setSecondsPlayed(const uint32_t seconds)
{
    secondsPlayed = seconds;
}

uint32_t DirectoryNode::getSecondsPlayed() const
{
    return secondsPlayed;
}

void DirectoryNode::buildDirectoryTree(const char *currentPath)
{
    // Clear existing data to prevent duplicates when rebuilding
    for (DirectoryNode *childNode : subdirectories)
    {
        delete childNode;
    }
    subdirectories.clear();
    mp3Files.clear();
    ids.clear();

    // Set directory path for this node (normalize: keep "/" or remove trailing slash)
    String path = String(currentPath);
    if (path.length() > 1 && path.endsWith("/"))
    {
        path.remove(path.length() - 1);
    }
    dirPath = path;

    // First collect entries so we can sort them alphabetically
    std::vector<String> dirNames;
    std::vector<String> fileNames;

    File rootDir = SD.open(currentPath);
    if (!rootDir)
    {
        Serial.print(F("buildDirectoryTree: failed to open path: "));
        Serial.println(currentPath);
        return;
    }
    while (true)
    {
        File entry = rootDir.openNextFile();
        if (!entry)
        {
            break;
        }

        if (entry.isDirectory() && entry.name()[0] != '.' && strcmp(entry.name(), sys_dir) != 0)
        {
            dirNames.emplace_back(entry.name());
        }
        else
        {
            String entryName = entry.name();
            if (entryName.endsWith(".mp3") || entryName.endsWith(".MP3"))
            {
                fileNames.push_back(std::move(entryName));
            }
        }
        entry.close();
    }
    rootDir.close();

    // Case-insensitive alphabetical sort without allocations
    auto ciLess = [](const String &a, const String &b) {
        const char* pa = a.c_str();
        const char* pb = b.c_str();
        while (*pa && *pb) {
            char ca = *pa++;
            char cb = *pb++;
            if (ca >= 'A' && ca <= 'Z') ca += 'a' - 'A';
            if (cb >= 'A' && cb <= 'Z') cb += 'a' - 'A';
            if (ca < cb) return true;
            if (ca > cb) return false;
        }
        return *pa < *pb;
    };
    std::sort(dirNames.begin(), dirNames.end(), ciLess);
    std::sort(fileNames.begin(), fileNames.end(), ciLess);

    // Reserve memory to reduce heap fragmentation
    subdirectories.reserve(dirNames.size());
    mp3Files.reserve(fileNames.size());
    ids.reserve(fileNames.size());

    // Add MP3 files in alphabetical order first to free fileNames memory before recursing
    for (const String &fileName : fileNames)
    {
        mp3Files.push_back(fileName);
        ids.push_back(getNextId());
    }
    // Free memory used by fileNames vector
    {
        std::vector<String> empty;
        fileNames.swap(empty);
    }

    // Create subdirectories in alphabetical order
    // Use index loop and std::move to free strings in dirNames as we go, reducing stack memory usage during recursion
    for (size_t i = 0; i < dirNames.size(); ++i)
    {
        String dirName = std::move(dirNames[i]); // Move string content out of vector
        
        DirectoryNode *newNode = new DirectoryNode(dirName);
        if (!newNode)
        {
            Serial.println(F("buildDirectoryTree: OOM creating DirectoryNode"));
            continue;
        }
        subdirectories.push_back(newNode);

        String childPath;
        childPath.reserve(dirPath.length() + 1 + dirName.length());
        if (dirPath == "/")
        {
            childPath = "/";
            childPath += dirName;
        }
        else
        {
            childPath = dirPath;
            childPath += "/";
            childPath += dirName;
        }

        newNode->buildDirectoryTree(childPath.c_str());
    }
}

void DirectoryNode::printDirectoryTree(int level) const
{
    for (int i = 0; i < level; i++)
    {
        Serial.print(F("  "));
    }
    Serial.println(name);
    
    for (const String &mp3File : mp3Files)
    {
        for (int i = 0; i <= level; i++)
        {
            Serial.print(F("  "));
        }
        Serial.println(mp3File);
    }
    
    for (DirectoryNode *childNode : subdirectories)
    {
        childNode->printDirectoryTree(level + 1);
    }
}

DirectoryNode *DirectoryNode::findFirstDirectoryWithMP3s()
{
    if (!mp3Files.empty())
    {
        // Found a directory with MP3 files
        return this;
    }

    for (DirectoryNode *subdirectory : subdirectories)
    {
        DirectoryNode *result = subdirectory->findFirstDirectoryWithMP3s();
        if (result != nullptr)
        {
            // Found a directory with MP3 files in the subdirectories
            return result;
        }
    }

    // No directory with MP3 files found
    return nullptr;
}

void DirectoryNode::advanceToFirstMP3InThisNode()
{
    if (mp3Files.size() > 0)
    {
        setCurrentPlaying(mp3Files[0]);
    }
}

DirectoryNode *DirectoryNode::advanceToMP3(const uint16_t id)
{
    // First check MP3 files in this directory
    for (size_t i = 0; i < ids.size(); i++)
    {
        if (id == ids[i])
        {
            // Found the current MP3 file
            buildFullPath(mp3Files[i], buffer, buffer_size);
            currentPlaying = String(buffer);  // Convert back to String for assignment
            currentPlayingId = id;
            return this;
        }
    }

    // Recursively search subdirectories
    for (auto subdir : subdirectories)
    {
        // Check if the ID matches a subdirectory ID
        if (subdir->getId() == id)
        {
            subdir->advanceToFirstMP3InThisNode();
            return subdir;
        }

        // Recursively search in subdirectory
        DirectoryNode *result = subdir->advanceToMP3(id);
        if (result != nullptr && !result->getCurrentPlaying().isEmpty())
        {
            return result;
        }
    }

    // If we get here, no song with this ID was found
    Serial.print(F("advanceToMP3: No song found for ID: "));
    Serial.println(id);
    return nullptr;
}

DirectoryNode *DirectoryNode::advanceToMP3(const String &songName)
{
    if (songName.isEmpty())
    {
        Serial.println(F("advanceToMP3: songName is empty"));
        return nullptr;
    }

    // Check if the input is an absolute path (starts with '/') or just a filename
    bool isAbsolutePath = songName.startsWith("/");
    // Normalize trailing slash for absolute folder path targets
    String normalizedPath = songName;
    if (isAbsolutePath && normalizedPath.length() > 1 && normalizedPath.endsWith("/"))
    {
        normalizedPath.remove(normalizedPath.length() - 1);
    }
    // Lowercased copies for case-insensitive comparisons (FAT can uppercase names)
    String lowTarget = songName;
    lowTarget.toLowerCase();

    // First, search in the current directory's MP3 files
    for (size_t i = 0; i < mp3Files.size(); i++)
    {
        if (isAbsolutePath)
        {
            // Use static buffer for path building and comparison
            buildFullPath(mp3Files[i], buffer, buffer_size);
            if (comparePathWithString(buffer, songName))
            {
                setCurrentPlaying(mp3Files[i]);
                return this;
            }
        }
        else
        {
            // Use static buffer for comparison without allocation
            buildFullPath(mp3Files[i], buffer, buffer_size);
            size_t bufLen = strlen(buffer);
            size_t targetLen = lowTarget.length();
            if (bufLen >= targetLen && strcasecmp(buffer + bufLen - targetLen, lowTarget.c_str()) == 0)
            {
                setCurrentPlaying(mp3Files[i]);
                return this;
            }
        }
    }

    // Then search in subdirectories
    for (auto subdir : subdirectories)
    {
        // Absolute folder target: match directory by its full path (dirPath)
        if (isAbsolutePath)
        {
            if (subdir->getDirPath().equalsIgnoreCase(normalizedPath))
            {
                subdir->advanceToFirstMP3InThisNode();
                return subdir;
            }
        }

        if (!isAbsolutePath && subdir->getName().equalsIgnoreCase(songName))
        {
            subdir->advanceToFirstMP3InThisNode();
            return subdir;
        }

        // Search all files within subdir:
        for (size_t i = 0; i < subdir->mp3Files.size(); i++)
        {

            if (isAbsolutePath)
            {
                if (subdir->buildFullPath(subdir->mp3Files[i]).equalsIgnoreCase(songName))
                {
                    subdir->setCurrentPlaying(subdir->mp3Files[i]);
                    return subdir;
                }
            }
            else
            {
                // Check suffix case-insensitively without creating new Strings
                const String& fName = subdir->mp3Files[i];
                size_t fLen = fName.length();
                size_t targetLen = lowTarget.length();
                if (fLen >= targetLen && strcasecmp(fName.c_str() + fLen - targetLen, lowTarget.c_str()) == 0)
                {
                    subdir->setCurrentPlaying(subdir->mp3Files[i]);
                    return subdir;
                }
            }
        }
        // Recurse into deeper subdirectories to support nested folders and files
        DirectoryNode* deeper = subdir->advanceToMP3(songName);
        if (deeper != nullptr)
        {
            return deeper;
        }
    }

    // If we get here, no matching song was found
    Serial.print(F("advanceToMP3: No song found for: "));
    Serial.println(songName);
    return nullptr;
}

/**
 * Moves to the previous MP3 file in the directory.
 * If the current song has been playing for more than a specific threshold, restarts the current song.
 * If the current song has just started, or it's the first song, moves to the previous song in the directory.
 *
 * @param thresholdSeconds The number of seconds to decide whether to restart the current song or go to the previous song.
 * @return A pointer to the DirectoryNode where the new current song is located, or nullptr if there's no previous song.
 */
DirectoryNode *DirectoryNode::goToPreviousMP3(uint32_t thresholdSeconds)
{
    // Safety check for null pointer
    if (currentPlaying.isEmpty())
    {
        Serial.println(F("goToPreviousMP3: currentPlaying is empty"));
        return nullptr;
    }

    // If we've been playing for more than threshold seconds, restart current song
    if (secondsPlayed > thresholdSeconds)
    {
        Serial.println(F("goToPreviousMP3: Restarting current song (played > threshold)"));
        return this;
    }

    // Find the current song index in this directory
    int currentIndex = -1;
    for (size_t i = 0; i < mp3Files.size(); i++)
    {
        buildFullPath(mp3Files[i], buffer, buffer_size);
        if (comparePathWithString(buffer, currentPlaying))
        {
            currentIndex = i;
            break;
        }
    }

    // If current song found and not the first song, move to previous
    if (currentIndex > 0)
    {
        Serial.print(F("goToPreviousMP3: Moving to previous song in same directory: "));
        Serial.println(mp3Files[currentIndex - 1]);
        setCurrentPlaying(mp3Files[currentIndex - 1]);
        return this;
    }

    // If we're at the first song or song not found in current directory,
    // we need to find the previous song globally
    Serial.println(F("goToPreviousMP3: At first song or song not found, looking for previous globally"));
    return nullptr; // Let the caller handle global previous logic
}

DirectoryNode *DirectoryNode::findPreviousMP3Globally(const String &currentGlobal)
{
    if (currentGlobal.isEmpty())
    {
        Serial.println(F("findPreviousMP3Globally: currentGlobal is null"));
        return nullptr;
    }

    // Build a flat list of all MP3 files in order
    std::vector<std::pair<DirectoryNode *, int>> allMP3s;
    buildFlatMP3List(allMP3s);

    // Find current song in the flat list
    int currentGlobalIndex = -1;
    for (size_t i = 0; i < allMP3s.size(); i++)
    {
        DirectoryNode *node = allMP3s[i].first;
        int fileIndex = allMP3s[i].second;
        
        node->buildFullPath(node->mp3Files[fileIndex], buffer, buffer_size);
        if (comparePathWithString(buffer, currentGlobal))
        {
            currentGlobalIndex = i;
            break;
        }
    }

    // If current song found and not the first globally, move to previous
    if (currentGlobalIndex > 0)
    {
        DirectoryNode *prevNode = allMP3s[currentGlobalIndex - 1].first;
        int prevFileIndex = allMP3s[currentGlobalIndex - 1].second;
        
        prevNode->buildFullPath(prevNode->mp3Files[prevFileIndex], buffer, buffer_size);
        
        Serial.print(F("findPreviousMP3Globally: Moving to previous song globally: "));
        Serial.println(buffer);
        
        prevNode->setCurrentPlaying(prevNode->mp3Files[prevFileIndex]);
        return prevNode;
    }

    Serial.println(F("findPreviousMP3Globally: No previous song found globally"));
    return nullptr;
}

void DirectoryNode::buildFlatMP3List(std::vector<std::pair<DirectoryNode *, int>> &allMP3s)
{

#ifdef DEBUG      
    Serial.println("Building flat mp3 list for folder");
#endif
    // Pre-reserve to reduce reallocations
    allMP3s.reserve(allMP3s.size() + mp3Files.size());
    // Add all MP3 files from this directory
    for (size_t i = 0; i < mp3Files.size(); i++)
    {
        allMP3s.emplace_back(this, i);
    }

    // Recursively add MP3 files from subdirectories
    for (DirectoryNode *subdir : subdirectories)
    {
        subdir->buildFlatMP3List(allMP3s);
    }
}

size_t DirectoryNode::getNumOfFiles() const
{
    return subdirectories.size();
}

DirectoryNode *DirectoryNode::advanceToNextMP3(const String &currentGlobal)
{
    Serial.println(currentGlobal.c_str());

    // Build a flat list of all MP3 files in order to correctly find the next one across directories
    std::vector<std::pair<DirectoryNode *, int>> allMP3s;
    buildFlatMP3List(allMP3s);

    if (allMP3s.empty())
    {
        Serial.println(F("advanceToNextMP3: No MP3s found in tree"));
        currentPlaying = "";
        return this;
    }

    int currentIndex = -1;
    if (!currentGlobal.isEmpty())
    {
        for (size_t i = 0; i < allMP3s.size(); i++)
        {
            DirectoryNode *node = allMP3s[i].first;
            int fileIndex = allMP3s[i].second;
            
            node->buildFullPath(node->mp3Files[fileIndex], buffer, buffer_size);
            if (comparePathWithString(buffer, currentGlobal))
            {
                currentIndex = (int)i;
                break;
            }
        }
    }

    // If current song found and not the last one, move to next
    if (currentIndex >= 0 && currentIndex < (int)allMP3s.size() - 1)
    {
        DirectoryNode *nextNode = allMP3s[currentIndex + 1].first;
        int nextFileIndex = allMP3s[currentIndex + 1].second;
        
        nextNode->setCurrentPlaying(nextNode->mp3Files[nextFileIndex]);
        return nextNode;
    }
    
    // If not playing anything (start), play first
    if (currentIndex == -1 && currentGlobal.isEmpty())
    {
        DirectoryNode *nextNode = allMP3s[0].first;
        int nextFileIndex = allMP3s[0].second;
        nextNode->setCurrentPlaying(nextNode->mp3Files[nextFileIndex]);
        return nextNode;
    }

    // If we get here, either we are at the last song, or the current song was not found
    currentPlaying = "";
    Serial.println(F("no more nodes found"));
    return this;
}
