/*
 * OpenSubsonicPlayer
 * Goldenkrew3000 2025
 * License: GNU General Public License 3.0
 * Info: Gstreamer Handler
 */

#include <stdio.h>
#include <gst/gst.h>
#include <math.h>
#include <stdbool.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include "../configHandler.h"
#include "../discordrpc.h"
#include "../libopensubsonic/logger.h"
#include "../libopensubsonic/endpoint_getSong.h"
#include "../libopensubsonic/httpclient.h"
#include "playQueue.hpp"
#include "player.h"

// TESTING
#include "scrobbler_lastFm.h"

extern configHandler_config_t* configObj;
static int rc = 0;
GstElement *pipeline, *playbin, *filter_bin, *conv_in, *conv_out, *in_volume, *equalizer, *pitch, *reverb, *out_volume;
GstPad *sink_pad, *src_pad;
GstBus* bus;
guint bus_watch_id;
GMainLoop* loop;
bool isPlaying = false;

static void gst_playbin3_sourcesetup_callback(GstElement* playbin, GstElement* source, gpointer udata) {
    g_object_set(G_OBJECT(source), "user-agent", "OSSP/1.0 (avery@hojuix.org)", NULL);
}

static gboolean gst_bus_call(GstBus* bus, GstMessage* message, gpointer data) {
    GMainLoop* loop = (GMainLoop*)data;

    switch (GST_MESSAGE_TYPE(message)) {
        case GST_MESSAGE_EOS:
            logger_log_important(__func__, "[GBus] End of stream");
            OSSPQ_advancePos(); // Move to next song in queue
            gst_element_set_state(pipeline, GST_STATE_NULL);
            isPlaying = false;
            break;
        case GST_MESSAGE_BUFFERING: {
            gint percent = 0;
            gst_message_parse_buffering(message, &percent);
            //printf("Buffering (%d%%)...\n", (int)percent);
            break;
        }
        case GST_MESSAGE_ERROR: {
            gchar* debug;
            GError* error;
            gst_message_parse_error(message, &error, &debug);
            printf("Gstreamer Error: %s\n", error->message);
            g_error_free(error);
            g_free(debug);
            break;
        }
        case GST_MESSAGE_STATE_CHANGED:
            //printf("State changed\n");
            break;
        case GST_MESSAGE_NEW_CLOCK:
            //
            break;
        case GST_MESSAGE_LATENCY:
            //
            break;
        case GST_MESSAGE_STREAM_START:
            //
            break;
        case GST_MESSAGE_ELEMENT:
            //
            break;
        case GST_MESSAGE_ASYNC_DONE:
            //
            break;
        case GST_MESSAGE_STREAM_STATUS:
            //
            break;
        case GST_MESSAGE_STREAMS_SELECTED:
            //
            break;
        case GST_MESSAGE_STREAM_COLLECTION:
            //
            break;
        case GST_MESSAGE_DURATION_CHANGED:
            //
            break;
        case GST_MESSAGE_TAG:
            // Unused
            break;
        default:
            printf("Unknown Message. Type %ld\n", GST_MESSAGE_TYPE(message));
            break;
    }

    return TRUE;
}

void* OSSPlayer_GMainLoop(void* arg) {
    (void)arg;
    logger_log_important(__func__, "GMainLoop thread running.");
    // This is needed for the Gstreamer bus to work, but it hangs the thread
    g_main_loop_run(loop);
}

void* OSSPlayer_ThrdInit(void* arg) {
    (void)arg;
    bool haveIssuedDiscordRPCIdle = true;
    bool haveScrobbledSong = false;

    // Player init function for pthread entry
    logger_log_important(__func__, "Player thread running.");
    OSSPlayer_GstInit();

    // Launch GMainLoop thread
    pthread_t pthr_gml;
    pthread_create(&pthr_gml, NULL, OSSPlayer_GMainLoop, NULL);

    // Poll play queue for new items to play
    while (true) { // TODO use global bool instead
        if (OSSPQ_getTotalPos() != 0 &&
            OSSPQ_getCurrentPos() != OSSPQ_getTotalPos() &&
            isPlaying == false) {
            // Player is not playing and a song is in the song queue

            // Pull new song from the song queue
            OSSPQ_SongStruct* songObject = OSSPQ_getAtPos(OSSPQ_getCurrentPos());
            if (songObject == NULL) {
                // Severe error - There was an item in the queue, but fetching it didn't work
                printf("[OSSPlayer]\n");
                // TODO: this
            }

            // Reset scrobble
            haveScrobbledSong = false;

            if (songObject->mode == OSSPQ_MODE_INTERNETRADIO) {
                // Setup Discord RPC
                discordrpc_data* discordrpc = NULL;
                discordrpc_struct_init(&discordrpc);
                discordrpc->state = DISCORDRPC_STATE_PLAYING_INTERNETRADIO;
                discordrpc->startTime = time(NULL);
                discordrpc->songTitle = strdup(songObject->title);
                discordrpc_update(&discordrpc);
                discordrpc_struct_deinit(&discordrpc);

                // Configure playbin3, and start playing
                g_object_set(playbin, "uri", songObject->streamUrl, NULL);
                //opensubsonic_httpClient_URL_cleanup(&stream_url);
                isPlaying = true;
                gst_element_set_state(pipeline, GST_STATE_PLAYING);
            } else if (songObject->mode == OSSPQ_MODE_OPENSUBSONIC) {
                // Issue initial LastFM scrobble
                scrobbler_data* scrobblerData = malloc(sizeof(scrobbler_data));
                opensubsonic_scrobble_init(scrobblerData);
                scrobblerData->songTitle = strdup(songObject->title);
                scrobblerData->songAlbum = strdup(songObject->album);
                scrobblerData->songArtist = strdup(songObject->artist);
                opensubsonic_scrobble_lastFm(scrobblerData);
                opensubsonic_scrobble_free(scrobblerData);

                // Prepare Discord RPC
                discordrpc_data* discordrpc = NULL;
                discordrpc_struct_init(&discordrpc);
                discordrpc->state = DISCORDRPC_STATE_PLAYING_OPENSUBSONIC;
                discordrpc->startTime = time(NULL);
                discordrpc->songLength = songObject->duration;
                discordrpc->songTitle = strdup(songObject->title);
                discordrpc->songArtist = strdup(songObject->artist);
                if (configObj->discordrpc_showCoverArt) {
                    discordrpc->coverArtUrl = strdup(songObject->coverArtUrl);
                }
                discordrpc_update(&discordrpc);
                discordrpc_struct_deinit(&discordrpc);

                // Free song queue object
                //OSSPQ_FreeSongObjectC(songObject);

                // Configure discord RPC idle boolean for when a song isn't playing
                haveIssuedDiscordRPCIdle = false;

                // Configure playbin3, free stream URL, send discord RPC, and start playing
                g_object_set(playbin, "uri", songObject->streamUrl, NULL);
                OSSPQ_FreeSongObjectC(songObject);
                isPlaying = true;
                gst_element_set_state(pipeline, GST_STATE_PLAYING);
            } else if (songObject->mode == OSSPQ_MODE_LOCALFILE) {
                // Prepare Discord RPC
                discordrpc_data* discordrpc = NULL;
                discordrpc_struct_init(&discordrpc);
                discordrpc->state = DISCORDRPC_STATE_PLAYING_LOCALFILE;
                discordrpc->startTime = time(NULL);
                discordrpc->songLength = songObject->duration;
                discordrpc->songTitle = strdup(songObject->title);
                discordrpc->songArtist = strdup(songObject->artist);
                discordrpc_update(&discordrpc);
                discordrpc_struct_deinit(&discordrpc);

                haveIssuedDiscordRPCIdle = false;

                // Configure playbin3, free stream URL, send discord RPC, and start playing
                g_object_set(playbin, "uri", songObject->streamUrl, NULL);
                OSSPQ_FreeSongObjectC(songObject);
                isPlaying = true;
                gst_element_set_state(pipeline, GST_STATE_PLAYING);
            }
        }

        if (OSSPQ_getCurrentPos() == OSSPQ_getTotalPos() && isPlaying == false) {
            // No song currently playing, and the queue is empty

            // Only send idle Discord RPC if needed to avoid spamming
            if (!haveIssuedDiscordRPCIdle) {
                printf("Issuing idle Discord RPC\n");
                haveIssuedDiscordRPCIdle = true;

                discordrpc_data* discordrpc = NULL;
                discordrpc_struct_init(&discordrpc);
                discordrpc->state = DISCORDRPC_STATE_IDLE;
                discordrpc_update(&discordrpc);
                discordrpc_struct_deinit(&discordrpc);
            }
        }

        // Scrobbler
        // Nothing playing: 0.00
        // Oh and end of song (EOS) -> 0.00
        
        // If song is >3/4 finished, perform final scrobble
        // Else, perform an in-progress scrobble every 45s (This can be handled later)
        // Have to fetch the total playback from Gstreamer, otherwise im malloc'ing every 200ms, a little fucking dramatic

        // NOTE: Cannot query Playbin3 for the length, as the OpenSubsonic /stream endpoint seems to be technically livestreaming it

        // Bad idea: Could technically do it the same way the DiscordRPC one does it
        // Gather the data at the same time, send a couple arguments and encapsulate it in it's own thread...
        // Kinda wasteful of a process though

        if (isPlaying == true) {
            float songLength = (float)OSSPQ_getSongLength(OSSPQ_getCurrentPos());
            printf("Song length: %f\n", songLength);
            printf("Current: %f\n", OSSPlayer_GstECont_Playbin3_Position_Get());

            // Check if song is >=3/4 finished
            if (OSSPlayer_GstECont_Playbin3_Position_Get() >= (songLength / 4 * 3) && haveScrobbledSong == false) {
                // Finalize song scrobble
                OSSPQ_SongStruct* songObject = OSSPQ_getAtPos(OSSPQ_getCurrentPos());

                scrobbler_data* scrobblerData = malloc(sizeof(scrobbler_data));
                opensubsonic_scrobble_init(scrobblerData);
                scrobblerData->finalize = 1;
                scrobblerData->songTitle = strdup(songObject->title);
                scrobblerData->songAlbum = strdup(songObject->album);
                scrobblerData->songArtist = strdup(songObject->artist);
                opensubsonic_scrobble_lastFm(scrobblerData);
                opensubsonic_scrobble_free(scrobblerData);

                haveScrobbledSong = true;
            }
        }


        usleep(200 * 1000);
    }
}

int OSSPlayer_GstInit() {
    printf("[OSSP] Initializing Gstreamer...\n");

    // Initialize gstreamer
    gst_init(NULL, NULL);
    loop = g_main_loop_new(NULL, FALSE);

    // Create base pipeline elements
    pipeline = gst_pipeline_new("pipeline");
    playbin = gst_element_factory_make("playbin3", "player");
    // TODO: Fix erroring
    if (!pipeline) {
        logger_log_error(__func__, "Could not initialize pipeline.");
    }
    if (!playbin) {
        logger_log_error(__func__, "Could not initialize playbin3");
    }

    // Add message handler
    bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
    // TODO: Check bus is made properly
    bus_watch_id = gst_bus_add_watch(bus, gst_bus_call, loop);
    gst_object_unref(bus);


    filter_bin = gst_bin_new("filter-bin");
    conv_in = gst_element_factory_make("audioconvert", "convert-in");
    conv_out = gst_element_factory_make("audioconvert", "convert-out");
    // TODO: Check creation

    // Create configuration defined elements
    in_volume = gst_element_factory_make("volume", "in-volume");
    if (configObj->audio_equalizer_enable) {
        // LSP Para x32 LR Equalizer
        equalizer = gst_element_factory_make(configObj->lv2_parax32_filter_name, "equalizer");
    }
    if (configObj->audio_pitch_enable) {
        // Soundtouch Pitch
        pitch = gst_element_factory_make("pitch", "pitch");
    }
    if (configObj->audio_reverb_enable) {
        // Calf Studio Plugins Reverb
        reverb = gst_element_factory_make(configObj->lv2_reverb_filter_name, "reverb");
    }
    out_volume = gst_element_factory_make("volume", "out-volume");
    // TODO: Make better error messages for here, and exit out early
    if (!equalizer) {
        logger_log_error(__func__, "Could not initialize equalizer.");
    }
    if (!pitch) {
        logger_log_error(__func__, "Could not initialize pitch.");
    }
    if (!reverb) {
        logger_log_error(__func__, "Could not initialize reverb.");
    }

    // Add and link elements to the filter bin
    // TODO: Check creation and dynamic as per config
    gst_bin_add_many(GST_BIN(filter_bin), conv_in, in_volume, equalizer, pitch, out_volume, conv_out, NULL);
    gst_element_link_many(conv_in, in_volume, equalizer, pitch, out_volume, conv_out, NULL);
    sink_pad = gst_element_get_static_pad(conv_in, "sink");
    src_pad = gst_element_get_static_pad(conv_out, "src");
    gst_element_add_pad(filter_bin, gst_ghost_pad_new("sink", sink_pad));
    gst_element_add_pad(filter_bin, gst_ghost_pad_new("src", src_pad));
    gst_object_unref(sink_pad);
    gst_object_unref(src_pad);

    // Setup playbin3 (Configure audio plugins and set user agent)
    g_object_set(playbin, "audio-filter", filter_bin, NULL);
    g_signal_connect(playbin, "source-setup", G_CALLBACK(gst_playbin3_sourcesetup_callback), NULL);

    // Add playbin3 to the pipeline
    gst_bin_add(GST_BIN(pipeline), playbin);

    // Initialize in-volume (Volume before the audio reaches the plugins)
    g_object_set(in_volume, "volume", 0.175, NULL);

    // Initialize out-volume (Volume after the audio plugins)
    g_object_set(out_volume, "volume", 1.00, NULL);

    // Initialize equalizer
    if (configObj->audio_equalizer_enable) {
        // Dynamically append settings to the equalizer to match the config file
        for (int i = 0; i < configObj->audio_equalizer_graphCount; i++) {
            char* ftl_name = NULL;
            char* ftr_name = NULL;
            char* gl_name = NULL;
            char* gr_name = NULL;
            char* ql_name = NULL;
            char* qr_name = NULL;
            char* fl_name = NULL;
            char* fr_name = NULL;

            asprintf(&ftl_name, "%s%d", configObj->lv2_parax32_filter_type_left, i);
            asprintf(&ftr_name, "%s%d", configObj->lv2_parax32_filter_type_right, i);
            asprintf(&gl_name, "%s%d", configObj->lv2_parax32_gain_left, i);
            asprintf(&gr_name, "%s%d", configObj->lv2_parax32_gain_right, i);
            asprintf(&ql_name, "%s%d", configObj->lv2_parax32_quality_left, i);
            asprintf(&qr_name, "%s%d", configObj->lv2_parax32_quality_right, i);
            asprintf(&fl_name, "%s%d", configObj->lv2_parax32_frequency_left, i);
            asprintf(&fr_name, "%s%d", configObj->lv2_parax32_frequency_right, i);

            g_object_set(equalizer, ftl_name, 1, NULL);
            g_object_set(equalizer, ftr_name, 1, NULL);

            // NOTE: Making an extra variable here to avoid nesting a function within a function
            float gain = (float)configObj->audio_equalizer_graph[i].gain;
            gain = OSSPlayer_DbLinMul(gain);
            g_object_set(equalizer, gl_name, gain, NULL);
            g_object_set(equalizer, gr_name, gain, NULL);

            g_object_set(equalizer, ql_name, 4.36, NULL);
            g_object_set(equalizer, qr_name, 4.36, NULL);

            // NOTE: Same function nesting mitigation here
            if (configObj->audio_equalizer_followPitch) {
                // Adjust equalizer frequency to match pitch adjustment
                // TODO: Should I also check if pitch is enabled, or just if pitch follow is enabled??
                // TODO: Also check that freq following is working properly as per swift version
                float freq = (float)configObj->audio_equalizer_graph[i].frequency;
                float semitone = (float)configObj->audio_pitch_cents / 100.0;
                freq = OSSPlayer_PitchFollow(freq, semitone);
                printf("EQ band %d - F: %.2f(Fp) / G: %.2f / Q: 4.36\n", i + 1, freq, gain);
                g_object_set(equalizer, fl_name, freq, NULL);
                g_object_set(equalizer, fr_name, freq, NULL);
            } else {
                printf("EQ band %d - F: %.2f(Nfp) / G: %.2f / Q: 4.36\n", i + 1, (float)configObj->audio_equalizer_graph[i].frequency, gain);
                g_object_set(equalizer, fl_name, (float)configObj->audio_equalizer_graph[i].frequency, NULL);
                g_object_set(equalizer, fr_name, (float)configObj->audio_equalizer_graph[i].frequency, NULL);
            }

            free(ftl_name);
            free(ftr_name);
            free(gl_name);
            free(gr_name);
            free(ql_name);
            free(qr_name);
            free(fl_name);
            free(fr_name);
        }

        g_object_set(equalizer, "enabled", true, NULL);
    }

    // Initialize pitch
    if (configObj->audio_pitch_enable) {
        float scaleFactor = OSSPlayer_CentsToPSF(configObj->audio_pitch_cents);
        printf("Pitch Cents: %.2f, Scale factor: %.6f\n", configObj->audio_pitch_cents, scaleFactor);
        g_object_set(pitch, "pitch", scaleFactor, NULL);
    }

    // Initialize reverb


}

int OSSPlayer_GstDeInit() {
    //
}

/*
 * Player Queue Control Functions
 */
int OSSPlayer_QueueAppend_Song(char* title, char* artist, char* id, long duration) {
    // Call to C++ function
    // Note: I would receive a song struct instead of individual elements, but it would significantly slow down the GUI
    //
}

int OSSPlayer_QueueAppend_Radio(char* name, char* id, char* radioUrl) {
    // Call to C++ function
    // Append radio station to the play queue
    //internal_OSSPQ_AppendToEnd(name, NULL, id, 0, 1, radioUrl);
}

OSSPQ_SongStruct* OSSPlayer_QueuePopFront() {
    // Call to C++ function

    //OSSPQ_SongStruct* songObject = internal_OSSPQ_PopFromFront();

    //if (songObject == NULL) {
        // Queue is empty TODO
        //printf("FUCKFUCKFUCK\n");
    //}
    //return songObject;
}

/*
 * Gstreamer Element Control Functions
 */
// TODO: Consolidate volume functions?
float OSSPlayer_GstECont_InVolume_Get() {
    gdouble vol;
    g_object_get(in_volume, "volume", &vol, NULL);
    return (float)vol;
}

void OSSPlayer_GstECont_InVolume_set(float val) {
    g_object_set(in_volume, "volume", val, NULL);
}

float OSSPlayer_GstECont_OutVolume_Get() {
    gdouble vol;
    g_object_get(out_volume, "volume", &vol, NULL);
    return (float)vol;
}

void OSSPlayer_GstECont_OutVolume_set(float val) {
    g_object_set(out_volume, "volume", val, NULL);
}

float OSSPlayer_GstECont_Playbin3_Position_Get() {
    gint64 seek_pos;
    if (gst_element_query_position(playbin, GST_FORMAT_TIME, &seek_pos)) {
        return (float)seek_pos / GST_SECOND;
    } else {
        return (float)0.00;
    }
}

void OSSPLayer_GstECont_Playbin3_Position_Set(float seek_pos) {
    gst_element_seek_simple(playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, (gint64)(seek_pos * GST_SECOND));
}

float OSSPlayer_GstECont_Pitch_Get() {
    //
}

void OSSPlayer_GstECont_Pitch_Set(float cents) {
    float psf = OSSPlayer_CentsToPSF(cents);
    g_object_set(pitch, "pitch", psf, NULL);
}

void OSSPlayer_GstECont_Playbin3_Stop() {
    OSSPQ_advancePos();
    gst_element_set_state(pipeline, GST_STATE_NULL); // Stop playbin3
    isPlaying = false; // Notify player thread to attempt to load next song
}

void OSSPlayer_GstECont_Playbin3_PlayPause() {
    GstState state;
    gst_element_get_state(pipeline, &state, NULL, 0);

    if (state == GST_STATE_PLAYING) {
        gst_element_set_state (pipeline, GST_STATE_PAUSED);

        // Issue Pause to Discord RPC
        OSSPlayer_DiscordRPC_SendPaused();
    } else {
        gst_element_set_state (pipeline, GST_STATE_PLAYING);

        // Get current position in song, and find start time for Discord RPC
        float curr_pos = OSSPlayer_GstECont_Playbin3_Position_Get();
        time_t curr_time = time(NULL);
        time_t start_time = curr_time - (int)curr_pos;

        // Issue Playing to Discord RPC
        OSSPlayer_DiscordRPC_SendPlaying(start_time);
    }
}

void OSSPlayer_GstECont_Playbin3_Prev() {
    // Move queue back by one, then stop playbin3 and notify player thread
    printf("[OSSPP] Moving the player queue back by one.\n");
    OSSPQ_backtrackPos();
    gst_element_set_state(pipeline, GST_STATE_NULL);
    isPlaying = false;
}

void OSSPlayer_GstECont_Playbin3_Next() {
    // Same as *Playbin3_Prev(), but advance queue by one
    printf("[OSSPP] Moving the player forward back by one.\n");
    OSSPQ_advancePos();
    gst_element_set_state(pipeline, GST_STATE_NULL);
    isPlaying = false;
}

void OSSPlayer_GstECont_Playbin3_StartQueue() {
    //
}

void OSSPlayer_GstECont_Playbin3_EndQueue() {
    //
}

/*
 * Utility Functions
 */
float OSSPlayer_DbLinMul(float db) {
    // Convert dB to Linear Multiplier
    return pow(10.0, db / 20.0);
}

float OSSPlayer_PitchFollow(float freq, float semitone) {
    // Calculate new EQ frequency from semitone adjustment
    return freq * pow(2.0, semitone / 12.0);
}

float OSSPlayer_CentsToPSF(float cents) {
    // Convert Cents to a Pitch Scale Factor
    float semitone = cents / 100.0;
    return pow(2, (semitone / 12.0f));
}

/*
 * Functions that utilize Discord RPC
 */
void OSSPlayer_DiscordRPC_SendPaused() {
    discordrpc_data* discordrpc = NULL;
    discordrpc_struct_init(&discordrpc);
    discordrpc->state = DISCORDRPC_STATE_PAUSED;
    discordrpc_update(&discordrpc);
    discordrpc_struct_deinit(&discordrpc);
}

void OSSPlayer_DiscordRPC_SendIdle() {
    discordrpc_data* discordrpc = NULL;
    discordrpc_struct_init(&discordrpc);
    discordrpc->state = DISCORDRPC_STATE_PAUSED;
    discordrpc_update(&discordrpc);
    discordrpc_struct_deinit(&discordrpc);
}

void OSSPlayer_DiscordRPC_SendPlaying(time_t startTime) {
    OSSPQ_SongStruct* songObject = OSSPQ_getAtPos(OSSPQ_getCurrentPos());
    if (songObject == NULL) {
        // Severe error - There was an item in the queue, but fetching it didn't work
        printf("[OSSPlayer]\n");
        // TODO: this
    }

    if (songObject->mode == OSSPQ_MODE_OPENSUBSONIC) {
        // Prepare Discord RPC
        discordrpc_data* discordrpc = NULL;
        discordrpc_struct_init(&discordrpc);
        discordrpc->state = DISCORDRPC_STATE_PLAYING_OPENSUBSONIC;
        discordrpc->startTime = startTime;
        discordrpc->songLength = songObject->duration;
        discordrpc->songTitle = strdup(songObject->title);
        discordrpc->songArtist = strdup(songObject->artist);
        if (configObj->discordrpc_showCoverArt) {
            discordrpc->coverArtUrl = strdup(songObject->coverArtUrl);
        }
        discordrpc_update(&discordrpc);
        discordrpc_struct_deinit(&discordrpc);
    }

    OSSPQ_FreeSongObjectC(songObject);
}

/*
 * Functions that utilize scrobblers
 */
void OSSPlayer_Scrobbler_LastFM(int final) {
    //
}
