
/*******************************************************************************/
/* Copyright (C) 2008-2020 Jonathan Moore Liles (as "Non-Session-Manager")     */
/* Copyright (C) 2020- Nils Hilbricht                                          */
/*                                                                             */
/* This file is part of New-Session-Manager                                    */
/*                                                                             */
/* New-Session-Manager is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by        */
/* the Free Software Foundation, either version 3 of the License, or           */
/* (at your option) any later version.                                         */
/*                                                                             */
/* New-Session-Manager is distributed in the hope that it will be useful,      */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of              */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               */
/* GNU General Public License for more details.                                */
/*                                                                             */
/* You should have received a copy of the GNU General Public License           */
/* along with New-Session-Manager. If not, see <https://www.gnu.org/licenses/>.*/
/*******************************************************************************/

#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-result"

#define _MODULE_ "nsm-proxy"
#define APP_NAME "NSM Proxy"
#define APP_TITLE "NSM Proxy"

#include "debug.h"

#include <lo/lo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/signalfd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <getopt.h>

static lo_server losrv;
static lo_address nsm_addr;
static lo_address gui_addr;
static int nsm_is_active;
static char *project_file;
static int die_now = 0;
static int signal_fd;

static char *nsm_client_id;
static char *nsm_display_name;

#define CONFIG_FILE_NAME "nsm-proxy.config"

void show_gui ( void );

class NSM_Proxy {

    char *_label;
    char *_executable;
    char *_config_file;
    char *_arguments;
    int _save_signal;
    int _stop_signal;
    int _pid;
    char *_client_error;

public:

    int stop_signal ( void ) {return _stop_signal;}

    NSM_Proxy ( )
        {
            _label = _executable = _arguments = _config_file = 0;
            _save_signal = 0;
            _stop_signal = SIGTERM;
            _pid = 0;
            _client_error = 0;
        }

    ~NSM_Proxy ( )
        {
        }

    void handle_client_death ( int status )
        {
            printf( "proxied process died unexpectedly... not dying\n" );
            /* proxied process died unexpectedly */

            if ( _client_error != NULL )
                free(_client_error);

            asprintf(&_client_error, "The proxied process terminated abnormally during invocation. Exit status: %i.", status );

            show_gui();

            _pid = 0;
        }

    void kill ( void )
        {
            if ( _pid )
            {
                ::kill( _pid, _stop_signal );
            }
        }

    bool start ( const char *executable, const char *arguments, const char *config_file )
        {
            if ( _executable )
                free( _executable );
            if ( _arguments )
                free( _arguments );
            if ( _config_file )
                free( _config_file );

            _executable = strdup( executable );

            if ( arguments )
                _arguments = strdup( arguments );
            else
                _arguments = NULL;

            if ( config_file )
                _config_file = strdup( config_file );
            else
                _config_file = NULL;

            return start();
        }

    bool start ( void )
        {
            dump( project_file );

            if ( _pid )
                /* already running */
                return true;

            if ( !_executable )
            {
                WARNING( "Executable is null." );
                return false;
            }

            int pid;
            if ( ! (pid = fork()) )
            {
                MESSAGE( "Launching %s\n", _executable );

//                char *args[] = { strdup( executable ), NULL };

                char *cmd;

                if ( _arguments )
                    asprintf( &cmd, "exec %s %s >error.log 2>&1", _executable, _arguments );
                else
                    asprintf( &cmd, "exec %s  >error.log 2>&1", _executable );

                char *args[] = { strdup("/bin/sh"), strdup( "-c" ), cmd, NULL };

                setenv( "NSM_CLIENT_ID", nsm_client_id, 1 );
                setenv( "NSM_SESSION_NAME", nsm_display_name, 1 );
                if ( _config_file )
                    setenv( "CONFIG_FILE", _config_file, 1 );
                unsetenv( "NSM_URL" );

                if ( -1 == execvp( "/bin/sh", args ) )
                {
                    WARNING( "Error starting process: %s", strerror( errno ) );
                    exit(1);
                }
            }

            _pid = pid;

            return _pid > 0;
        }

    void save_signal ( int s )
        {
            _save_signal = s;
        }

    void stop_signal ( int s )
        {
            _stop_signal = s;
        }

    void label ( const char *s )
        {
            if ( _label )
                free( _label );

            _label = strdup( s );

            lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/nsm/client/label", "s", _label );
        }

    void save ( void )
        {
            MESSAGE( "Sending process save signal" );
            if ( _pid )
                ::kill( _pid, _save_signal );
        }


    bool dump ( const char *path )
        {
            //Dump current config to file. This happens quite often.
            char *fname;
            asprintf( &fname, "%s/%s", path, CONFIG_FILE_NAME );

            FILE *fp = fopen( fname, "w" );

            free( fname );

            if ( !fp )
            {
                WARNING( "Error opening file for saving: %s", strerror( errno ) );
                return false;
            }

            if ( _executable && strlen(_executable) )
                fprintf( fp, "executable\n\t%s\n", _executable );

            if ( _arguments && strlen(_arguments) )
                fprintf( fp, "arguments\n\t%s\n", _arguments );

            if ( _config_file && strlen(_config_file) )
                fprintf( fp, "config file\n\t%s\n", _config_file );

            fprintf( fp, "save signal\n\t%i\n", _save_signal );

            fprintf( fp, "stop signal\n\t%i\n", _stop_signal );

            if ( _label && strlen(_label) )
                fprintf( fp, "label\n\t%s\n", _label );

            fclose( fp );

            return true;
        }

    bool restore ( const char *path )
        {
            FILE *fp = fopen( path, "r" );
            if ( ! fp )
            {
                WARNING( "Error opening file for restore: %s", strerror( errno ) );
                return false;
            }

            char *name;
            char *value;

            MESSAGE( "Loading file config \"%s\"", path );

            while ( 2 == fscanf( fp, "%m[^\n]\n\t%m[^\n]\n", &name, &value ) )
            {

                MESSAGE( "%s=%s", name, value );

                if ( !strcmp( name, "executable" ) )
                    _executable = value;
                else if (!strcmp( name, "arguments" ) )
                    _arguments = value;
                else if (!strcmp( name, "config file" ) )
                    _config_file = value;
                else if ( !strcmp( name, "save signal" ) )
                {
                    _save_signal = atoi( value );
                    free( value );
                }
                else if ( !strcmp( name, "stop signal" ) )
                {
                    _stop_signal = atoi( value );
                    free( value );
                }
                else if ( !strcmp( name, "label" ) )
                {
                    label( value );
                    free( value );
                }
                else
                {
                    WARNING( "Unknown option \"%s\" in config file", name );
                }

                free( name );
            }

            fclose( fp );

            start();

            return true;
        }

    void update ( lo_address to )
        {
            //Each send triggers one osc_update in the Proxy-GUI.
            MESSAGE( "Sending update" );

            lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/save_signal", "i",  _save_signal );
            lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/label", "s", _label ? _label : "" );
            lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/executable", "s", _executable ? _executable : "" );
            lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/arguments", "s", _arguments ? _arguments : "" );
            lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/config_file", "s", _config_file ? _config_file : "" );
            lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/stop_signal", "i",  _stop_signal );
            lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/proxy/client_error", "s",  _client_error ? _client_error : "" );
        }
};

NSM_Proxy *nsm_proxy;

bool
snapshot ( const char *file )
{
    return nsm_proxy->dump(file);
}
void
announce ( const char *nsm_url, const char *client_name, const char *process_name )
{
    printf( "Announcing to NSM\n" );

    lo_address to = lo_address_new_from_url( nsm_url );

    int pid = (int)getpid();

    lo_send_from( to, losrv, LO_TT_IMMEDIATE, "/nsm/server/announce", "sssiii",
                  client_name,
                  ":optional-gui:",
                  process_name,
                  1, /* api_major_version */
                  0, /* api_minor_version */
                  pid );

    lo_address_free( to );
}

bool
open ( const char *file )
{
    char *path;
    asprintf( &path, "%s/%s", file, CONFIG_FILE_NAME );

    bool r = nsm_proxy->restore( path );

    free( path );

    return r;
}

/****************/
/* OSC HANDLERS */
/****************/

/* NSM */

int
osc_announce_error ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    if ( strcmp( types, "sis" ) )
        return -1;

    if ( strcmp( "/nsm/server/announce", &argv[0]->s ) )
         return -1;

    printf( "Failed to register with NSM: %s\n", &argv[2]->s );
    nsm_is_active = 0;

    return 0;
}


int
osc_announce_reply ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    if ( strcmp( "/nsm/server/announce", &argv[0]->s ) )
         return -1;

    printf( "Successfully registered. NSM says: %s", &argv[1]->s );

    nsm_is_active = 1;
    nsm_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) ) );

    return 0;
}

int
osc_save ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    bool r = snapshot( project_file );

    nsm_proxy->save();

    if ( r )
        lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/reply", "ss", path, "OK" );
    else
        lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/error", "sis", path, -1, "Error saving project file" );

    return 0;
}

static int gui_pid;

void
show_gui ( void )
{

    int pid;
    if ( ! (pid = fork()) )
    {
        char executable[] = "nsm-proxy-gui";

        MESSAGE( "Launching %s\n", executable );

        char *url = lo_server_get_url( losrv );

        char *args[] = { executable, strdup( "--connect-to" ), url, NULL };

        if ( -1 == execvp( executable, args ) )
        {
            WARNING( "Error starting process: %s", strerror( errno ) );

            exit(1);
        }
    }

    gui_pid = pid;

    lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/nsm/client/gui_is_shown", "" );
}

int
osc_show_gui ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    show_gui();

    /* FIXME: detect errors */

    lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/reply", "ss", path, "OK" );

    return 0;
}

void
hide_gui ( void )
{
    if ( gui_pid )
    {
        kill( gui_pid, SIGTERM );
    }
}

int
osc_hide_gui ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    hide_gui();

    lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/nsm/client/gui_is_hidden", "" );

    /* FIXME: detect errors */

    lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/reply", "ss", path, "OK" );

    return 0;
}

int
osc_open ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    const char *new_path = &argv[0]->s;
    const char *display_name = &argv[1]->s;
    const char *client_id = &argv[2]->s;

    if ( nsm_client_id )
        free(nsm_client_id);

    nsm_client_id = strdup( client_id );

    if ( nsm_display_name )
        free( nsm_display_name );

    nsm_display_name = strdup( display_name );

    char *new_filename;

    mkdir( new_path, 0777 );

    chdir( new_path );

    asprintf( &new_filename, "%s/%s", new_path, CONFIG_FILE_NAME );

    struct stat st;

    if ( 0 == stat( new_filename, &st ) )
    {
        if ( open( new_path ) )
        {
        }
        else
        {
            lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/error", "sis", path, -1, "Could not open file" );
            return 0;
        }

        lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/nsm/client/gui_is_hidden", "" );
    }
    else
    {
        show_gui();
    }

    if ( project_file )
        free( project_file );

    project_file = strdup( new_path );

// new_filename;

    lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/reply", "ss", path, "OK" );

    if ( gui_addr )
        nsm_proxy->update( gui_addr );

    return 0;
}



/* GUI */

int
osc_label ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    nsm_proxy->label( &argv[0]->s );

    return 0;
}

int
osc_save_signal ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    nsm_proxy->save_signal( argv[0]->i );

    return 0;
}

int
osc_stop_signal ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    nsm_proxy->stop_signal( argv[0]->i );

    return 0;
}

int
osc_start ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    snapshot( project_file );

    if ( nsm_proxy->start( &argv[0]->s, &argv[1]->s, &argv[2]->s ) )
    {
        hide_gui();
    }

    return 0;
}

int
osc_kill ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    nsm_proxy->kill();

    return 0;
}

int
osc_update ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data )
{
    lo_address to = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) ));

    nsm_proxy->update( to );

    gui_addr = to;

    return 0;
}



void
signal_handler ( int x )
{
    die_now = 1;
}

void
set_traps ( void )
{
    signal( SIGHUP, signal_handler );
    signal( SIGINT, signal_handler );
//  signal( SIGQUIT, signal_handler );
//  signal( SIGSEGV, signal_handler );
//  signal( SIGPIPE, signal_handler );
    signal( SIGTERM, signal_handler );
}


void
init_osc ( const char *osc_port )
{
    losrv = lo_server_new( osc_port, NULL );
//error_handler );

    char *url = lo_server_get_url(losrv);
    printf("OSC: %s\n",url);
    free(url);

    /* NSM */
    lo_server_add_method( losrv, "/nsm/client/save", "", osc_save, NULL );
    lo_server_add_method( losrv, "/nsm/client/open", "sss", osc_open, NULL );
    lo_server_add_method( losrv, "/nsm/client/show_optional_gui", "", osc_show_gui, NULL );
    lo_server_add_method( losrv, "/nsm/client/hide_optional_gui", "", osc_hide_gui, NULL );
    lo_server_add_method( losrv, "/error", "sis", osc_announce_error, NULL );
    lo_server_add_method( losrv, "/reply", "ssss", osc_announce_reply, NULL );

    /* GUI */
    lo_server_add_method( losrv, "/nsm/proxy/label", "s", osc_label, NULL );
    lo_server_add_method( losrv, "/nsm/proxy/save_signal", "i", osc_save_signal, NULL );
    lo_server_add_method( losrv, "/nsm/proxy/stop_signal", "i", osc_stop_signal, NULL );
    lo_server_add_method( losrv, "/nsm/proxy/kill", "", osc_kill, NULL );
    lo_server_add_method( losrv, "/nsm/proxy/start", "sss", osc_start, NULL );
    lo_server_add_method( losrv, "/nsm/proxy/update", "", osc_update, NULL );

}

void
die ( void )
{
    if ( gui_pid )
    {
        MESSAGE( "Killing GUI" );

        kill( gui_pid, SIGTERM );
    }

    nsm_proxy->kill();

    exit(0);
}


void handle_sigchld ( )
{
    for ( ;; )
    {
        int status;
        pid_t pid = waitpid(-1, &status, WNOHANG);

        if (pid <= 0)
            break;

        if ( pid == gui_pid )
        {
            lo_send_from( nsm_addr, losrv, LO_TT_IMMEDIATE, "/nsm/client/gui_is_hidden", "" );

            gui_pid = 0;

            /* don't care... */
            continue;
        }

        if ( WIFSIGNALED(status)  )
        {
            /* process was killed via signal */
            if (WTERMSIG(status) == SIGTERM ||
                WTERMSIG(status) == SIGHUP ||
                WTERMSIG(status) == SIGINT ||
                WTERMSIG(status) == SIGKILL )
            {
                /* process was killed via an appropriate signal */
                MESSAGE( "child was killed (maybe by us)\n" );
                die_now = 1;
                continue;
            }
        }
        else if ( WIFEXITED(status) )
        {
            /* child called exit() or returned from main() */

            MESSAGE( "child exit status: %i", WEXITSTATUS(status) );

            if ( WEXITSTATUS(status) == 0 )
            {
                /* apparently normal termination */
                MESSAGE( "child exited without error.");
                die_now = 1;
                continue;
            }
            else
            {
                MESSAGE("child exited abnormally.");
                nsm_proxy->handle_client_death(WEXITSTATUS(status));
            }
        }
    }
}

int
main ( int argc, char **argv )
{
    set_traps();

    sigset_t mask;

    sigemptyset( &mask );
    sigaddset( &mask, SIGCHLD );

    sigprocmask(SIG_BLOCK, &mask, NULL );

    signal_fd = signalfd( -1, &mask, SFD_NONBLOCK );

    //Command line parameters
    static struct option long_options[] =
    {
        { "help", no_argument, 0, 'h' },
        { 0, 0, 0, 0 }
    };
    int option_index = 0;
    int c = 0;
    while ( ( c = getopt_long_only( argc, argv, "", long_options, &option_index  ) ) != -1 )
    {
        switch ( c )
        {
            case 'h':
                {
                const char *usage =
                "nsm-proxy - Wrapper for executables without direct NSM-Support.\n\n"
                "It is a module for the 'New Session Manager' and only communicates\n"
                "over OSC in an NSM-Session and has no standalone functionality.\n"
                "\n"
                "Usage:\n"
                "  nsm-proxy --help\n"
                "\n"
                "Options:\n"
                "  --help                Show this screen\n"
                "";
                puts ( usage );
                exit(0);
                break;
                }
        }
    }



    nsm_proxy = new NSM_Proxy();

    init_osc( NULL );

    const char *nsm_url = getenv( "NSM_URL" );

    if ( nsm_url )
    {
        announce( nsm_url, APP_TITLE, argv[0] );
    }
    else
    {
        fprintf( stderr, "Could not register as NSM client.\n" );
        exit(1);
    }


    struct signalfd_siginfo fdsi;

    /* listen for sigchld signals and process OSC messages forever */
    for ( ;; )
    {
        ssize_t s = read(signal_fd, &fdsi, sizeof(struct signalfd_siginfo));

        if (s == sizeof(struct signalfd_siginfo))
        {
            if (fdsi.ssi_signo == SIGCHLD)
                handle_sigchld();
        }

        lo_server_recv_noblock( losrv, 500 );

        if ( die_now )
            die();
    }
}
