#include "shredblob_live.hpp"

#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sstream>
#include <algorithm>
#include <fstream>
#include <cctype>
#include <vector>

LiveParams gLive = LiveParams::defaults();

static int gListenFd = -1;
static int gClientFd = -1;
static pid_t gPanelPid = 0;
static std::string gRecvBuf;
static std::string gSocketPath;

LiveParams LiveParams::defaults() {
    return LiveParams{};
}

static void setVec3(glm::vec3& v, float r, float g, float b) {
    v = glm::vec3(r, g, b);
}

void LiveParams::applyKeyValue(const std::string& key, float a, float b, float c, int components) {
    if (key == "bpm") bpm = a;
    else if (key == "time_scale") timeScale = a;
    else if (key == "narrative_duration") narrativeDuration = a;
    else if (key == "network_start") networkStart = a;
    else if (key == "freed_bounce_duration") freedBounceDuration = a;
    else if (key == "tunnel_duration") tunnelDuration = a;
    else if (key == "launch_duration") launchDuration = a;
    else if (key == "finale_white_duration") finaleWhiteDuration = a;
    else if (key == "link_brightness") linkBrightness = a;
    else if (key == "learn_rate") learnRate = a;
    else if (key == "music_drive_gain") musicDriveGain = a;
    else if (key == "beat_sensitivity") beatSensitivity = a;
    else if (key == "tunnel_speed_gain") tunnelSpeedGain = a;
    else if (key == "launch_thrust_gain") launchThrustGain = a;
    else if (key == "star_swirl_gain") starSwirlGain = a;
    else if (key == "camera_follow_min") cameraFollowMin = a;
    else if (key == "camera_follow_max") cameraFollowMax = a;
    else if (key == "blob_scale") blobScale = a;
    else if (key == "tunnel_behind_mul") tunnelBehindMul = a;
    else if (key == "tunnel_ahead_mul") tunnelAheadMul = a;
    else if (key == "dendrite_line_width") dendriteLineWidth = a;
    else if (key == "fov_tunnel") fovTunnel = a;
    else if (key == "fov_launch") fovLaunch = a;
    else if (key == "cube_half_extent") cubeHalfExtent = a;
    else if (key == "reset_demo") resetDemo = (a >= 0.5f);
    else if (key == "jump_phase") jumpPhase = a;
    else if (components >= 3) {
        if (key == "bg_color") setVec3(bgColor, a, b, c);
        else if (key == "color_fire") setVec3(colorFire, a, b, c);
        else if (key == "color_amber") setVec3(colorAmber, a, b, c);
        else if (key == "color_chrome") setVec3(colorChrome, a, b, c);
        else if (key == "tunnel_warm") setVec3(tunnelWarm, a, b, c);
        else if (key == "tunnel_hot") setVec3(tunnelHot, a, b, c);
        else if (key == "tunnel_cool") setVec3(tunnelCool, a, b, c);
        else if (key == "impact_color") setVec3(impactColor, a, b, c);
        else if (key == "link_warm") setVec3(linkWarm, a, b, c);
        else if (key == "star_warm") setVec3(starWarm, a, b, c);
    }
}

std::string LiveParams::serialize() const {
    std::ostringstream os;
    auto vec = [&](const char* k, const glm::vec3& v) {
        os << k << '=' << v.r << ' ' << v.g << ' ' << v.b << '\n';
    };
    auto f = [&](const char* k, float v) { os << k << '=' << v << '\n'; };
    f("bpm", bpm);
    f("time_scale", timeScale);
    f("narrative_duration", narrativeDuration);
    f("network_start", networkStart);
    f("freed_bounce_duration", freedBounceDuration);
    f("tunnel_duration", tunnelDuration);
    f("launch_duration", launchDuration);
    f("finale_white_duration", finaleWhiteDuration);
    vec("bg_color", bgColor);
    vec("color_fire", colorFire);
    vec("color_amber", colorAmber);
    vec("color_chrome", colorChrome);
    vec("tunnel_warm", tunnelWarm);
    vec("tunnel_hot", tunnelHot);
    vec("tunnel_cool", tunnelCool);
    vec("impact_color", impactColor);
    vec("link_warm", linkWarm);
    vec("star_warm", starWarm);
    f("link_brightness", linkBrightness);
    f("learn_rate", learnRate);
    f("music_drive_gain", musicDriveGain);
    f("beat_sensitivity", beatSensitivity);
    f("tunnel_speed_gain", tunnelSpeedGain);
    f("launch_thrust_gain", launchThrustGain);
    f("star_swirl_gain", starSwirlGain);
    f("camera_follow_min", cameraFollowMin);
    f("camera_follow_max", cameraFollowMax);
    f("blob_scale", blobScale);
    f("tunnel_behind_mul", tunnelBehindMul);
    f("tunnel_ahead_mul", tunnelAheadMul);
    f("dendrite_line_width", dendriteLineWidth);
    f("fov_tunnel", fovTunnel);
    f("fov_launch", fovLaunch);
    f("cube_half_extent", cubeHalfExtent);
    os << ".\n";
    return os.str();
}

static void parseMessage(const std::string& msg) {
    std::istringstream in(msg);
    std::string line;
    while (std::getline(in, line)) {
        if (line.empty() || line == ".") continue;
        auto eq = line.find('=');
        if (eq == std::string::npos) continue;
        std::string key = line.substr(0, eq);
        std::string val = line.substr(eq + 1);
        std::istringstream vs(val);
        float a = 0, b = 0, c = 0;
        vs >> a;
        if (vs >> b) {
            if (vs >> c)
                gLive.applyKeyValue(key, a, b, c, 3);
            else
                gLive.applyKeyValue(key, a, b, 0, 2);
        } else {
            gLive.applyKeyValue(key, a, 0, 0, 1);
        }
    }
    liveTuneApplyBlobScale();
}

static void setNonBlocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags >= 0)
        fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

bool liveTuneStartServer(std::string& outSocketPath) {
    gListenFd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (gListenFd < 0) return false;

    gSocketPath = "/tmp/shredblob_" + std::to_string(getpid()) + ".live";
    unlink(gSocketPath.c_str());

    sockaddr_un addr{};
    addr.sun_family = AF_UNIX;
    std::snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", gSocketPath.c_str());
    if (bind(gListenFd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
        close(gListenFd);
        gListenFd = -1;
        return false;
    }
    listen(gListenFd, 1);
    setNonBlocking(gListenFd);
    outSocketPath = gSocketPath;
    printf("Live tuning socket: %s\n", gSocketPath.c_str());
    return true;
}

void liveTunePoll() {
    if (gListenFd < 0) return;

    if (gClientFd < 0) {
        int fd = accept(gListenFd, nullptr, nullptr);
        if (fd >= 0) {
            gClientFd = fd;
            setNonBlocking(gClientFd);
            std::string hello = gLive.serialize();
            send(gClientFd, hello.data(), hello.size(), MSG_NOSIGNAL);
        }
    }

    if (gClientFd >= 0) {
        char buf[4096];
        for (;;) {
            ssize_t n = recv(gClientFd, buf, sizeof(buf), 0);
            if (n > 0) {
                gRecvBuf.append(buf, buf + n);
                for (;;) {
                    auto end = gRecvBuf.find("\n.\n");
                    if (end == std::string::npos) break;
                    parseMessage(gRecvBuf.substr(0, end));
                    gRecvBuf.erase(0, end + 3);
                }
            } else if (n == 0) {
                close(gClientFd);
                gClientFd = -1;
                break;
            } else {
                break;
            }
        }
    }
}

bool liveTuneSpawnPanel(const std::string& socketPath) {
    pid_t pid = fork();
    if (pid < 0) return false;
    if (pid == 0) {
        execlp("python3", "python3", "shredblob_controls.py", socketPath.c_str(), (char*)nullptr);
        _exit(127);
    }
    gPanelPid = pid;
    return true;
}

void liveTuneShutdown() {
    if (gPanelPid > 0) {
        kill(gPanelPid, SIGTERM);
        gPanelPid = 0;
    }
    if (gClientFd >= 0) {
        close(gClientFd);
        gClientFd = -1;
    }
    if (gListenFd >= 0) {
        close(gListenFd);
        gListenFd = -1;
    }
    if (!gSocketPath.empty()) {
        unlink(gSocketPath.c_str());
        gSocketPath.clear();
    }
}

void liveTuneApplyBlobScale() {
    extern float blobScale;
    blobScale = gLive.blobScale;
}

static bool jsonExtractScalar(const std::string& json, const std::string& key, float& out) {
    const std::string needle = "\"" + key + "\"";
    size_t pos = json.find(needle);
    if (pos == std::string::npos) return false;
    pos = json.find(':', pos + needle.size());
    if (pos == std::string::npos) return false;
    ++pos;
    while (pos < json.size() && std::isspace(static_cast<unsigned char>(json[pos]))) ++pos;
    char* end = nullptr;
    out = std::strtof(json.c_str() + pos, &end);
    return end != json.c_str() + pos;
}

static bool jsonExtractVec3(const std::string& json, const std::string& key, float& r, float& g, float& b) {
    const std::string needle = "\"" + key + "\"";
    size_t pos = json.find(needle);
    if (pos == std::string::npos) return false;
    pos = json.find('[', pos + needle.size());
    if (pos == std::string::npos) return false;
    ++pos;
    char* end = nullptr;
    r = std::strtof(json.c_str() + pos, &end);
    if (end == json.c_str() + pos) return false;
    pos = end - json.c_str();
    while (pos < json.size() && json[pos] != ',' && json[pos] != ']') ++pos;
    if (pos >= json.size() || json[pos] != ',') return false;
    ++pos;
    g = std::strtof(json.c_str() + pos, &end);
    if (end == json.c_str() + pos) return false;
    pos = end - json.c_str();
    while (pos < json.size() && json[pos] != ',' && json[pos] != ']') ++pos;
    if (pos >= json.size() || json[pos] != ',') return false;
    ++pos;
    b = std::strtof(json.c_str() + pos, &end);
    return end != json.c_str() + pos;
}

static std::string executableDirectory();

static std::string gLastPresetPath;
static std::string gPresetMp3Raw;

static bool fileReadable(const std::string& path) {
    std::ifstream f(path);
    return f.good();
}

static bool jsonExtractString(const std::string& json, const std::string& key, std::string& out) {
    const std::string needle = "\"" + key + "\"";
    size_t pos = json.find(needle);
    if (pos == std::string::npos) return false;
    pos = json.find(':', pos + needle.size());
    if (pos == std::string::npos) return false;
    pos = json.find('"', pos);
    if (pos == std::string::npos) return false;
    ++pos;
    out.clear();
    while (pos < json.size() && json[pos] != '"') {
        if (json[pos] == '\\' && pos + 1 < json.size()) {
            ++pos;
            if (json[pos] == 'n') out += '\n';
            else if (json[pos] == 't') out += '\t';
            else out += json[pos];
        } else {
            out += json[pos];
        }
        ++pos;
    }
    return !out.empty();
}

static std::string resolvePathRelativeTo(const std::string& raw, const std::string& baseDir) {
    if (raw.empty()) return "";
    if (raw[0] == '/' && fileReadable(raw)) return raw;
    if (baseDir.empty()) return raw;
    std::string joined = baseDir + "/" + raw;
    if (fileReadable(joined)) return joined;
    return raw;
}

std::string liveTunePresetMp3Path() {
    if (gPresetMp3Raw.empty()) return "";
    if (gPresetMp3Raw[0] == '/' && fileReadable(gPresetMp3Raw)) return gPresetMp3Raw;

    std::string presetDir = ".";
    if (!gLastPresetPath.empty()) {
        auto slash = gLastPresetPath.rfind('/');
        presetDir = (slash != std::string::npos) ? gLastPresetPath.substr(0, slash) : ".";
    }

    std::string resolved = resolvePathRelativeTo(gPresetMp3Raw, presetDir);
    if (fileReadable(resolved)) return resolved;
    if (fileReadable(gPresetMp3Raw)) return gPresetMp3Raw;
    resolved = resolvePathRelativeTo(gPresetMp3Raw, executableDirectory());
    if (fileReadable(resolved)) return resolved;
    return gPresetMp3Raw;
}

static void applyLoadedPresetFromJson(const std::string& json) {
    float v = 0.0f;
    if (jsonExtractScalar(json, "bpm", v)) gLive.bpm = v;
    if (jsonExtractScalar(json, "time_scale", v)) gLive.timeScale = v;
    if (jsonExtractScalar(json, "narrative_duration", v)) gLive.narrativeDuration = v;
    if (jsonExtractScalar(json, "network_start", v)) gLive.networkStart = v;
    if (jsonExtractScalar(json, "freed_bounce_duration", v)) gLive.freedBounceDuration = v;
    if (jsonExtractScalar(json, "tunnel_duration", v)) gLive.tunnelDuration = v;
    if (jsonExtractScalar(json, "launch_duration", v)) gLive.launchDuration = v;
    if (jsonExtractScalar(json, "finale_white_duration", v)) gLive.finaleWhiteDuration = v;
    if (jsonExtractScalar(json, "link_brightness", v)) gLive.linkBrightness = v;
    if (jsonExtractScalar(json, "learn_rate", v)) gLive.learnRate = v;
    if (jsonExtractScalar(json, "music_drive_gain", v)) gLive.musicDriveGain = v;
    if (jsonExtractScalar(json, "beat_sensitivity", v)) gLive.beatSensitivity = v;
    if (jsonExtractScalar(json, "tunnel_speed_gain", v)) gLive.tunnelSpeedGain = v;
    if (jsonExtractScalar(json, "launch_thrust_gain", v)) gLive.launchThrustGain = v;
    if (jsonExtractScalar(json, "star_swirl_gain", v)) gLive.starSwirlGain = v;
    if (jsonExtractScalar(json, "camera_follow_min", v)) gLive.cameraFollowMin = v;
    if (jsonExtractScalar(json, "camera_follow_max", v)) gLive.cameraFollowMax = v;
    if (jsonExtractScalar(json, "blob_scale", v)) gLive.blobScale = v;
    if (jsonExtractScalar(json, "tunnel_behind_mul", v)) gLive.tunnelBehindMul = v;
    if (jsonExtractScalar(json, "tunnel_ahead_mul", v)) gLive.tunnelAheadMul = v;
    if (jsonExtractScalar(json, "dendrite_line_width", v)) gLive.dendriteLineWidth = v;
    if (jsonExtractScalar(json, "fov_tunnel", v)) gLive.fovTunnel = v;
    if (jsonExtractScalar(json, "fov_launch", v)) gLive.fovLaunch = v;
    if (jsonExtractScalar(json, "cube_half_extent", v)) gLive.cubeHalfExtent = v;

    float r = 0.0f, g = 0.0f, b = 0.0f;
    if (jsonExtractVec3(json, "bg_color", r, g, b)) gLive.bgColor = glm::vec3(r, g, b);
    if (jsonExtractVec3(json, "color_fire", r, g, b)) gLive.colorFire = glm::vec3(r, g, b);
    if (jsonExtractVec3(json, "color_amber", r, g, b)) gLive.colorAmber = glm::vec3(r, g, b);
    if (jsonExtractVec3(json, "color_chrome", r, g, b)) gLive.colorChrome = glm::vec3(r, g, b);
    if (jsonExtractVec3(json, "tunnel_warm", r, g, b)) gLive.tunnelWarm = glm::vec3(r, g, b);
    if (jsonExtractVec3(json, "tunnel_hot", r, g, b)) gLive.tunnelHot = glm::vec3(r, g, b);
    if (jsonExtractVec3(json, "tunnel_cool", r, g, b)) gLive.tunnelCool = glm::vec3(r, g, b);
    if (jsonExtractVec3(json, "impact_color", r, g, b)) gLive.impactColor = glm::vec3(r, g, b);
    if (jsonExtractVec3(json, "link_warm", r, g, b)) gLive.linkWarm = glm::vec3(r, g, b);
    if (jsonExtractVec3(json, "star_warm", r, g, b)) gLive.starWarm = glm::vec3(r, g, b);
}

static std::string executableDirectory() {
#ifdef __linux__
    char buf[4096];
    ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        std::string path(buf);
        auto slash = path.rfind('/');
        if (slash != std::string::npos)
            return path.substr(0, slash);
    }
#endif
    return ".";
}

static std::vector<std::string> defaultPresetSearchPaths() {
    std::vector<std::string> paths;
    paths.push_back("shredblob_tune.json");
    paths.push_back(executableDirectory() + "/shredblob_tune.json");
    if (const char* home = std::getenv("HOME")) {
        paths.push_back(std::string(home) + "/.config/shredblob/shredblob_tune.json");
    }
    if (const char* xdg = std::getenv("XDG_CONFIG_HOME")) {
        paths.push_back(std::string(xdg) + "/shredblob/shredblob_tune.json");
    }
    return paths;
}

bool liveTuneLoadPresetFile(const std::string& path) {
    std::ifstream in(path);
    if (!in) return false;
    std::string json((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
    if (json.empty()) return false;
    gLastPresetPath = path;
    gPresetMp3Raw.clear();
    if (!jsonExtractString(json, "mp3", gPresetMp3Raw))
        jsonExtractString(json, "audio_file", gPresetMp3Raw);
    applyLoadedPresetFromJson(json);
    liveTuneApplyBlobScale();
    return true;
}

bool liveTuneLoadDefaultPreset(std::string* outLoadedPath) {
    for (const std::string& path : defaultPresetSearchPaths()) {
        if (liveTuneLoadPresetFile(path)) {
            if (outLoadedPath) *outLoadedPath = path;
            return true;
        }
    }
    return false;
}
