//  This file is distributed as part of the bit-babbler package.
//  Copyright 2012 - 2016,  Ron <ron@debian.org>

#ifndef _REENTRANT
#error "seedd requires pthread support"
#endif

#include "private_setup.h"

#include <bit-babbler/socket-source.h>
#include <bit-babbler/secret-sink.h>
#include <bit-babbler/control-socket.h>
#include <bit-babbler/signals.h>

#include <bit-babbler/impl/health-monitor.h>
#include <bit-babbler/impl/log.h>

#include <getopt.h>


#if EM_PLATFORM_POSIX
 #define DEFAULT_CONTROL_SOCK   "/var/run/bit-babbler/seedd.socket"
#else
 #define DEFAULT_CONTROL_SOCK   "tcp:localhost:56789"
#endif


using BitB::BitBabbler;
using BitB::Pool;
using BitB::SocketSource;
using BitB::ControlSock;
using BitB::CreateControlSocket;
using BitB::SecretSink;
using BitB::StrToUL;
using BitB::StrToScaledUL;
using BitB::StrToScaledD;
using BitB::SystemError;
using BitB::Log;


static void usage()
{
    printf("Usage: seedd [OPTION...]\n");
    printf("\n");
    printf("Read entropy from BitBabbler hardware RNG devices\n");
    printf("\n");
    printf("Options:\n");
    printf("  -s, --scan                Scan for available devices\n");
    printf("      --shell-mr            Output a machine readable list of devices\n");
    printf("  -i, --device-id=id        Read from only the selected device(s)\n");
    printf("  -b, --bytes=n             Send n bytes to stdout\n");
    printf("  -d, --daemon              Run as a background daemon\n");
    printf("  -k, --kernel              Feed entropy to the kernel\n");
    printf("  -u, --udp-out=host:port   Provide a UDP socket for entropy output\n");
    printf("  -o, --stdout              Send entropy to stdout\n");
    printf("  -P, --pool-size=n         Size of the entropy pool\n");
    printf("  -G, --group-size=g:n      Size of a single pool group\n");
    printf("  -c, --control-socket=path Where to create the control socket\n");
    printf("      --socket-group=grp    Grant group access to the control socket\n");
    printf("      --watch=path:ms:bs:n  Monitor an external device\n");
    printf("      --kernel-refill=sec   Max time in seconds before OS pool refresh\n");
    printf("  -v, --verbose             Enable verbose output\n");
    printf("  -?, --help                Show this help message\n");
    printf("      --version             Print the program version\n");
    printf("\n");
    printf("Per device options:\n");
    printf("  -r, --bitrate=Hz          Set the bitrate (in bits per second)\n");
    printf("      --latency=ms          Override the USB latency timer\n");
    printf("  -f, --fold=n              Set the amount of entropy folding\n");
    printf("  -g, --group=n             The pool group to add the device to\n");
    printf("      --enable=mask         Select a subset of the generators\n");
    printf("      --idle-sleep=init:max Tune the rate of pool refresh when idle\n");
    printf("      --suspend-after=ms    Set the threshold for USB autosuspend\n");
    printf("      --low-power           Convenience preset for idle and suspend\n");
    printf("      --no-qa               Don't drop blocks that fail QA checking\n");
    printf("\n");
    printf("Report bugs to support@bitbabbler.org\n");
    printf("\n");
}


#if EM_PLATFORM_POSIX

void WriteCompletion( void *p )
{
    pthread_t   *t = static_cast<pthread_t*>( p );
    pthread_kill( *t, SIGRTMIN );
}

#else

static pthread_mutex_t  wait_mutex   = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t   wait_cond    = PTHREAD_COND_INITIALIZER;
static int              done_waiting = 0;

void WriteCompletion( void *p )
{
    (void)p;
    BitB::ScopedMutex   lock( &wait_mutex );
    done_waiting = 1;
    pthread_cond_broadcast( &wait_cond );
}

#endif

int main( int argc, char *argv[] )
{
  try {

    unsigned                    opt_scan        = 0;
    size_t                      opt_bytes       = 0;
    unsigned                    opt_daemon      = 0;
    unsigned                    opt_kernel      = 0;
    unsigned                    opt_stdout      = 0;
    std::string                 opt_controlsock;
    std::string                 opt_socketgroup;
    std::string                 opt_socket_source;

    Pool::Options               pool_options;
    Pool::Group::Options::List  group_options;
    BitBabbler::Options         default_options;
    BitBabbler::Options::List   device_options;
    SecretSink::Options::List   watch_options;

    enum
    {
        SHELL_MR_OPT,
        LATENCY_OPT,
        ENABLE_OPT,
        NOQA_OPT,
        SOCKET_GROUP_OPT,
        WATCH_OPT,
        KERNEL_REFILL_TIME_OPT,
        IDLE_SLEEP_OPT,
        SUSPEND_AFTER_OPT,
        LOW_POWER_OPT,
        VERSION_OPT
    };

    struct option long_options[] =
    {
        { "scan",           no_argument,        NULL,      's' },
        { "shell-mr",       no_argument,        NULL,      SHELL_MR_OPT },
        { "device-id",      required_argument,  NULL,      'i' },
        { "bitrate",        required_argument,  NULL,      'r' },
        { "latency",        required_argument,  NULL,      LATENCY_OPT },
        { "fold",           required_argument,  NULL,      'f' },
        { "group",          required_argument,  NULL,      'g' },
        { "enable",         required_argument,  NULL,      ENABLE_OPT },
        { "idle-sleep",     required_argument,  NULL,      IDLE_SLEEP_OPT },
        { "suspend-after",  required_argument,  NULL,      SUSPEND_AFTER_OPT },
        { "low-power",      no_argument,        NULL,      LOW_POWER_OPT },
        { "no-qa",          no_argument,        NULL,      NOQA_OPT },
        { "bytes",          required_argument,  NULL,      'b' },
        { "daemon",         no_argument,        NULL,      'd' },
        { "kernel",         no_argument,        NULL,      'k' },
        { "udp-out",        required_argument,  NULL,      'u' },
        { "stdout",         no_argument,        NULL,      'o' },
        { "pool-size",      required_argument,  NULL,      'P' },
        { "group-size",     required_argument,  NULL,      'G' },
        { "control-socket", required_argument,  NULL,      'c' },
        { "socket-group",   required_argument,  NULL,      SOCKET_GROUP_OPT },
        { "watch",          required_argument,  NULL,      WATCH_OPT },
        { "kernel-refill",  required_argument,  NULL,      KERNEL_REFILL_TIME_OPT },
        { "verbose",        no_argument,        NULL,      'v' },
        { "help",           no_argument,        NULL,      '?' },
        { "version",        no_argument,        NULL,      VERSION_OPT },
        { 0, 0, 0, 0 }
    };

    int opt_index = 0;

    for(;;)
    { //{{{

        int c = getopt_long( argc, argv, ":si:r:f:g:b:dku:oP:G:c:v?",
                                long_options, &opt_index );
        if( c == -1 )
            break;

        switch(c)
        {
            case 's':
                opt_scan = 1;
                break;

            case SHELL_MR_OPT:
                opt_scan = 2;
                break;

            case 'i':
            {
                BitBabbler::Options     bbo = default_options;

                try {
                    bbo.id = optarg;
                }
                catch( const std::exception &e )
                {
                    fprintf( stderr, "%s: error, %s\n", argv[0], e.what() );
                    return EXIT_FAILURE;
                }

                device_options.push_back( bbo );
                break;
            }

            case 'r':
            {
                unsigned bitrate = StrToScaledD( optarg );

                if( device_options.empty() )
                    default_options.bitrate = bitrate;
                else
                    device_options.back().bitrate = bitrate;

                break;
            }

            case LATENCY_OPT:
            {
                unsigned latency = StrToUL( optarg, 10 );

                if( device_options.empty() )
                    default_options.latency = latency;
                else
                    device_options.back().latency = latency;

                break;
            }

            case 'f':
            {
                unsigned fold = StrToUL( optarg, 10 );

                if( device_options.empty() )
                    default_options.fold = fold;
                else
                    device_options.back().fold = fold;

                break;
            }

            case 'g':
            {
                unsigned group = StrToUL( optarg, 10 );

                if( device_options.empty() )
                    default_options.group = group;
                else
                    device_options.back().group = group;

                break;
            }

            case 'c':
                opt_controlsock = optarg;
                break;

            case SOCKET_GROUP_OPT:
                opt_socketgroup = optarg;
                break;

            case ENABLE_OPT:
            {
                unsigned mask = StrToUL( optarg );

                if( device_options.empty() )
                    default_options.enable_mask = mask;
                else
                    device_options.back().enable_mask = mask;

                break;
            }

            case IDLE_SLEEP_OPT:
                try {
                    if( device_options.empty() )
                        default_options.SetIdleSleep( optarg );
                    else
                        device_options.back().SetIdleSleep( optarg );
                }
                catch( const std::exception &e )
                {
                    fprintf( stderr, "%s: error, %s\n", argv[0], e.what() );
                    return EXIT_FAILURE;
                }
                break;

            case SUSPEND_AFTER_OPT:
                if( device_options.empty() )
                    default_options.suspend_after = StrToScaledUL( optarg );
                else
                    device_options.back().suspend_after = StrToScaledUL( optarg );

                break;

            case LOW_POWER_OPT:
                if( device_options.empty() )
                {
                    default_options.SetIdleSleep( "100:0" );
                    default_options.suspend_after = 10000;
                } else {
                    device_options.back().SetIdleSleep( "100:0" );
                    device_options.back().suspend_after = 10000;
                }
                pool_options.kernel_refill_time = 3600;
                break;

            case NOQA_OPT:
                if( device_options.empty() )
                    default_options.no_qa = true;
                else
                    device_options.back().no_qa = true;

                break;

            case 'b':
                opt_bytes = StrToScaledUL( optarg, 1024 );
                break;

            case 'd':
                opt_daemon       = 1;
                opt_kernel       = 1;
                BitB::opt_syslog = 1;
                break;

            case 'k':
                opt_kernel = 1;
                break;

            case 'u':
                opt_socket_source = optarg;
                break;

            case 'o':
                opt_stdout = 1;
                break;

            case 'P':
                pool_options.pool_size = StrToScaledUL( optarg, 1024 );
                break;

            case KERNEL_REFILL_TIME_OPT:
                pool_options.kernel_refill_time = StrToUL( optarg, 10 );
                break;

            case 'G':
                group_options.push_back( Pool::Group::Options( optarg ) );
                break;

            case WATCH_OPT:
                watch_options.push_back( SecretSink::Options::ParseOptArg( optarg ) );
                break;

            case 'v':
                ++BitB::opt_verbose;
                break;

            case '?':
                if( optopt != '?' && optopt != 0 )
                {
                    fprintf(stderr, "%s: invalid option -- '%c', try --help\n",
                                                            argv[0], optopt);
                    return EXIT_FAILURE;
                }
                usage();
                return EXIT_SUCCESS;

            case ':':
                fprintf(stderr, "%s: missing argument for '%s', try --help\n",
                                                    argv[0], argv[optind - 1] );
                return EXIT_FAILURE;

            case VERSION_OPT:
                printf("seedd " PACKAGE_VERSION "\n");
                return EXIT_SUCCESS;
        }

    } //}}}


   #if EM_PLATFORM_POSIX

    if( BitB::opt_syslog )
        openlog( argv[0], LOG_PID, LOG_DAEMON );

    if( opt_daemon && ! opt_scan )
    {
        if( daemon(0,0) )
            throw SystemError( _("seedd: Failed to fork daemon") );

        umask( S_IWGRP | S_IROTH | S_IWOTH | S_IXOTH );
    }

    BitB::BlockSignals();

   #else
    (void)opt_daemon;
   #endif


    BitB::Devices   d;

    if( opt_scan )
    {
        switch( opt_scan )
        {
            case 1:
                d.ListDevices();
                return EXIT_SUCCESS;

            case 2:
                d.ListDevicesShellMR();
                return EXIT_SUCCESS;
        }

        fprintf(stderr, "seedd: unknown device scan option %u\n", opt_scan );
        return EXIT_FAILURE;
    }
    else if( d.GetNumDevices() == 0 && ! d.HasHotplugSupport() )
    {
        // If we don't have hotplug support, and we don't have any devices now,
        // then there's no point waiting around, because none will appear later.
        fprintf( stderr, _("seedd: No devices found, and no hotplug support.  Aborting.\n") );
        return EXIT_FAILURE;
    }


    pthread_t       main_thread = pthread_self();
    Pool::Handle    pool        = new Pool( pool_options );

    for( Pool::Group::Options::List::iterator i = group_options.begin(),
                                              e = group_options.end(); i != e; ++i )
        pool->AddGroup( i->groupid, i->size );

    d.AddDevicesToPool( pool, default_options, device_options );


    SocketSource::Handle    ssrc;

    if( ! opt_socket_source.empty() )
    {
        opt_bytes = 0;
        ssrc = new SocketSource( pool, opt_socket_source );
    }

    if( opt_kernel )
    {
        opt_bytes = 0;
        pool->FeedKernelEntropyAsync();
    }

    if( opt_stdout || opt_bytes )
    {
        if( opt_bytes && opt_controlsock.empty() )
            opt_controlsock = "none";

       #if EM_PLATFORM_MSW
        setmode( STDOUT_FILENO, O_BINARY );
       #endif
        pool->WriteToFDAsync( STDOUT_FILENO, opt_bytes, WriteCompletion, &main_thread );
    }

    SecretSink::List    watch_sinks;

    for( SecretSink::Options::List::iterator i = watch_options.begin(),
                                             e = watch_options.end(); i != e; ++i )
        watch_sinks.push_back( new SecretSink( *i ) );


    if( opt_controlsock.empty() )
        opt_controlsock = DEFAULT_CONTROL_SOCK;

    ControlSock::Handle ctl = CreateControlSocket( opt_controlsock, opt_socketgroup );


   #if EM_PLATFORM_POSIX

    int sig;

    wait_for_the_signal:
    sig = BitB::SigWait( SIGINT, SIGQUIT, SIGTERM, SIGABRT, SIGTSTP, SIGRTMIN, SIGUSR1 );

    switch( sig )
    {
        case SIGTSTP:
            Log<0>( _("Stopped by signal %d (%s)\n"), sig, strsignal(sig) );
            raise( SIGSTOP );
            goto wait_for_the_signal;

        case SIGUSR1:
            // p.ReportState()
            goto wait_for_the_signal;

        default:
            // We can't switch on SIGRTMIN, it's not a constant.
            if( sig == SIGRTMIN )
                Log<1>( _("Wrote %zu bytes to stdout\n"), opt_bytes );
            else
                Log<0>( _("Terminated by signal %d (%s)\n"), sig, strsignal(sig) );
            break;
    }

   #else

    BitB::ScopedMutex   lock( &wait_mutex );

    while( ! done_waiting )
        pthread_cond_wait( &wait_cond, &wait_mutex );

   #endif

    return EXIT_SUCCESS;
  }
  BB_CATCH_ALL( 0, _("seedd fatal exception") )

  return EXIT_FAILURE;
}

// vi:sts=4:sw=4:et:foldmethod=marker
