/* virttest.c -- test for creating/deleting virtual ports */
/*
 * Roger B. Dannenberg
 * Oct 2021

This test is performed by running 2 instances of the program.  The
first instance makes input and output ports named portmidi and waits
for a message. The second tries to do the same, but will fail because
portmidi already exists. It then opens portmidi (both input and
output). In greater detail:

FIRST INSTANCE           SECOND INSTANCE
--------------           ---------------

initialize PortMidi      initialize PortMidi
create portmidi in
create portmidi out
wait for input
                         create portmidi in -> fails
                         open portmidi in/out
                         send to portmidi
recv from portmidi
send to portmidi
wait 1s                  recv from portmidi
                         close portmidi in and out
                         terminate PortMidi
list all devices:
 - check for correct number
 - check for good description of portmidi in port (open)
 - check for good description of portmidi out port (open)
close portmidi in
list all devices:
 - check for correct number
 - check for good description of portmidi in port (closed)
 - check for good description of portmidi out port (open)
close portmidi out
list all devices:
 - check for correct number
 - check for good description of portmidi in port (closed)
 - check for good description of portmidi out port (closed)
delete portmidi in
 - check for correct number
 - check for NULL description of portmidi in port
 - check for good description of portmidi out port (closed)
delete portmidi out
 - check for correct number
 - check for NULL description of portmidi in port
 - check for NULL description of portmidi out port
terminate portmidi
REPEAT 3 TIMES           wait 2 seconds to give head start to other instance
                         REPEAT 3 TIMES
 */

#include "portmidi.h"
#include "porttime.h"
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include "assert.h"

#define OUTPUT_BUFFER_SIZE 0
#define INPUT_BUFFER_SIZE 10
#define DEVICE_INFO NULL
#define DRIVER_INFO NULL
#define TIME_PROC ((PmTimeProcPtr) Pt_Time)
#define TIME_INFO NULL
#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */


static void prompt_and_exit(void)
{
    printf("type ENTER...");
    while (getchar() != '\n') ;
    /* this will clean up open ports: */
    exit(-1);
}


static PmError printerror(PmError err, const char *msg)
{
    if (err == pmHostError) {
        /* it seems pointless to allocate memory and copy the string,
         * so I will do the work of Pm_GetHostErrorText directly
         */
        char errmsg[80];
        Pm_GetHostErrorText(errmsg, 80);
        printf("%s\n    %s\n", msg, errmsg);
    } else if (err < 0) {
        printf("%s\n    %s\n", msg, Pm_GetErrorText(err));
    }
    return err;
}


static PmError checkerror(PmError err)
{
    if (err < 0) {
        printerror(err, "PortMidi call failed...");
        prompt_and_exit();
    }
    return err;
}


void wait_until(PmTimestamp when)
{
    PtTimestamp now = Pt_Time();
    if (when > now) {
        Pt_Sleep(when - now);
    }
}


void show_usage(void)
{
    printf("Usage: virttest\n"
           "    run two instances to test virtual port create/delete\n");
}


void check_info(int id, char stat, int input, int virtual)
{
    const PmDeviceInfo *info = Pm_GetDeviceInfo(id);
    if (stat == 'd') {
        if (info) {
            printf("Expected device %d to be deleted.\n", id);
            prompt_and_exit();
        }
        return;
    }
    if (!info) {
        printf("Expected device %d to not be deleted.\n", id);
        prompt_and_exit();
    }
    if (strcmp("portmidi", info->name) != 0) {
        printf("Device %d name is %s, not \"portmidi\".\n", id, info->name);
        prompt_and_exit();
    }
    if (info->input != input || (!info->output) != input) {
        printf("Device %d input/output fields are wrong.\n", id);
        prompt_and_exit();
    }
    if ((!info->opened && stat == 'o') || (info->opened && stat == 'c')) {
        printf("Device %d opened==%d, status should be %c.\n", id,
               info->opened, stat);
        prompt_and_exit();
    }
    if (info->is_virtual != virtual) {
        printf("Expected device %d to be virtual.\n", id);
        prompt_and_exit();
    }
}


/* stat is 'o' for open, 'c' for closed, 'd' for deleted device */
void check_ports(int cnt, int in_id, char in_stat,
                 int out_id, char out_stat, int virtual)
{
    if (cnt != Pm_CountDevices()) {
        printf("Device count changed from %d to %d.\n", cnt, Pm_CountDevices());
        prompt_and_exit();
    }
    check_info(in_id, in_stat, TRUE, virtual);
    check_info(out_id, out_stat, FALSE, virtual);
}


void devices_list(void)
{
    int i;
    for (i = 0; i < Pm_CountDevices(); i++) {
        const PmDeviceInfo *info = Pm_GetDeviceInfo(i);
        if (info) {
            printf("%d: %s %s %s %s\n", i, info->name,
                   (info->input ? "input" : "output"),
                   (info->is_virtual ? "virtual" : "real_device"),
                   (info->opened ? "opened" : "closed"));
        }
    }
}


void test2(void)
{
    PmStream *out = NULL;
    PmStream *in = NULL;
    int out_id;
    int in_id;
    PmEvent buffer[1];
    PmTimestamp timestamp;
    int pitch = 60;
    int device_count = 0;
    int i;

    printf("This must be virttest instance #2\n");

    /* find and open portmidi in and out */
    device_count = Pm_CountDevices();
    for (i = 0; i < device_count; i++) {
        const PmDeviceInfo *info = Pm_GetDeviceInfo(i);
        if (info && strcmp(info->name, "portmidi") == 0) {
            if (info->input) {
                checkerror(Pm_OpenInput(&in, i, DRIVER_INFO,
                                   INPUT_BUFFER_SIZE, TIME_PROC, TIME_INFO));
                in_id = i;
            } else {
                checkerror(Pm_OpenOutput(&out, i, DRIVER_INFO,
                                         OUTPUT_BUFFER_SIZE, NULL, NULL, 0));
                out_id = i;
            }
        }
    }
    if (!in) {
        printf("Did not open portmidi as input (virtual output).\n");
        prompt_and_exit();
    }        
    if (!out) {
        printf("Did not open portmidi as output (virtual input).\n");
        prompt_and_exit();
    }        
    printf("Input device %d and output device %d are open.\n", in_id, out_id);

    /* send a message */
    buffer[0].timestamp = 0;
    buffer[0].message = Pm_Message(0x90, pitch, 100);
    checkerror(Pm_Write(out, buffer, 1));

    /* wait for reply */
    printf("Sent message, waiting for reply...\n");
    while (Pm_Read(in, buffer, 1) < 1) Pt_Sleep(10);
    
    printf("********** GOT THE MESSAGE, SHUTTING DOWN ************\n");

    /* close in */
    checkerror(Pm_Close(in));
    check_ports(device_count, in_id, 'c', out_id, 'o', FALSE);
    printf("Closed input %d\n", in_id);

    /* close out */
    checkerror(Pm_Close(out));
    check_ports(device_count, in_id, 'c', out_id, 'c', FALSE);
    printf("Closed output %d\n", out_id);

    Pt_Sleep(1000);
    /* wrap it up */
    Pm_Terminate();
    printf("Got reply and terminated...\n");
    Pt_Sleep(2000);  /* 2 seconds because other is waiting 1s. */
    /* 1 more second to make sure other shuts down before test repeats. */
}

extern int pm_check_errors;

void test(void)
{
    PmStream *out;
    PmStream *in;
    int out_id;
    int in_id;
    PmEvent buffer[1];
    PmTimestamp timestamp;
    int device_count = 0;

    TIME_START;

    printf("******** INITIALIZING PORTMIDI ***********\n");
    timestamp = Pt_Time();
    Pm_Initialize();
    printf("Pm_Initialize took %dms\n", Pt_Time() - timestamp);
    devices_list();

    pm_check_errors = FALSE;  /* otherwise, PM_CHECK_ERRORS, if defined,     */
    /* can cause this program to report an error and exit on pmNameConflict. */
    in_id = Pm_CreateVirtualInput("portmidi", NULL, DEVICE_INFO);
    pm_check_errors = TRUE;  /* there should be no other errors */
    if (in_id < 0) {
        printerror(in_id, "Pm_CreateVirtualInput failed...");
        test2();
        return;
    }
    printf("Created portmidi virtual input; this is virttest instance #1\n");
    out_id = checkerror(Pm_CreateVirtualOutput("portmidi", NULL, DRIVER_INFO));
    device_count = Pm_CountDevices();
    
    checkerror(Pm_OpenInput(&in, in_id, NULL, 0, NULL, NULL));
    checkerror(Pm_OpenOutput(&out, out_id, DRIVER_INFO, OUTPUT_BUFFER_SIZE,
                             TIME_PROC, TIME_INFO, 0));
    printf("Created/Opened input %d and output %d\n", in_id, out_id);
    Pm_SetFilter(in, PM_FILT_ACTIVE | PM_FILT_CLOCK | PM_FILT_SYSEX);
    /* empty the buffer after setting filter, just in case anything
       got through */
    while (Pm_Read(in, buffer, 1)) ;

    /* wait for input */
    printf("Waiting for input...\n");
    while (Pm_Read(in, buffer, 1) < 1) Pt_Sleep(10);

    /* send two replies (only one would be fine) */
    checkerror(Pm_Write(out, buffer, 1));
    printf("Received input, writing output...\n");

    /* wait 1s so receiver can get the message before we shut down */
    Pt_Sleep(1000);
    printf("****** Closing everything and shutting down...\n");

    /* expect 2 open ports */
    check_ports(device_count, in_id, 'o', out_id, 'o', TRUE);
    /* close in */
    checkerror(Pm_Close(in));
    check_ports(device_count, in_id, 'c', out_id, 'o', TRUE);

    /* close out */
    checkerror(Pm_Close(out));
    check_ports(device_count, in_id, 'c', out_id, 'c', TRUE);

    /* delete in */
    checkerror(Pm_DeleteVirtualDevice(in_id));
    check_ports(device_count, in_id, 'd', out_id, 'c', TRUE);

    /* delete out */
    checkerror(Pm_DeleteVirtualDevice(out_id));
    check_ports(device_count, in_id, 'd', out_id, 'd', TRUE);
    
    /* we are done */
    Pm_Terminate();
}


int main(int argc, char *argv[])
{
    int i;
    show_usage();
    for (i = 0; i < 3; i++) {
        test();
    }    
    printf("finished virttest (SUCCESS). Type ENTER to quit...");
    while (getchar() != '\n') ;
    return 0;
}
