/* MOXC -- a C version of Collinge's MOXIE language */
/* Copyright 1989 Carnegie Mellon University */

/*****************************************************************************
*       Change Log
*  Date     | Change
*-----------+-----------------------------------------------------------------
* 31-Dec-85 | Modified for use with midi
*  5-Feb-86 | Added m_rest and m_restuntil allowing rests at top level
* 28-May-86 | Added command line parsing
*  4-Jun-86 | changed keyevent to separate calls for each event type
* 10-Jul-86 | put loop in mainscore with prompt to play and replay
* 03-Jun-88 | modified for portability (AMIGA) -JCD
* 07-Jul-89 | time bases -RBD
* 31-Jan-90 | GWL : cleaned up for LATTICE
* 30-Jun-90 | RBD : further changes
*  2-Apr-91 | JDW : further changes
*  4-Mar-91 | GWL : DOS allows odd inst addrs
* 10-Oct-94 | nix : posicionador tridimensionale interface
* 28-Apr-03 |  DM : true->TRUE, false->FALSE
*****************************************************************************/

#include "switches.h"

#ifdef AMIGA
#ifdef AZTEC
#include "functions.h"
#else
#include "amiga.h"
#endif
#include "exec/exec.h"
#include "cmtcmd.h"

extern long event_mask; /* imported from midifns.c */
#endif
extern int abort_flag;  /*DMH: taken out of ifdef AMIGA for moxcrun*/

#include "stdio.h"
#include "cext.h"
#include "userio.h"
#include "midifns.h"

#include "cmdline.h"
#include "midicode.h"
#include "timebase.h"
#include "moxc.h"

#ifdef AMIGA /*DMH: only AMIGA cares about AMIGA's "proportional controllers"*/
#include "prop1.h"
#endif
#ifdef POSICIONADOR_3D
#include "pos3d.h"
#include "pos3dbuf.h"
#endif /* POSICIONADOR_3D */

extern char *app_syntax;

/***************************************************************************
*
*  IMPORTS:
*       asciievent(k)           user-defined action for terminal input
*       bendchange(ch, val)     user-defined pitch bend handler
*       ctrlchange(ch, c, val)  user-defined control change handler
*       keydown(ch, p, v)       user-defined MIDI note on handler
*       keyup(ch, p)            user-defined MIDI note off handler
*       mainscore()             user-defined first action(s)
*       musicfns                lots of time and io functions
*       peddown(ch)             user-defined pedal down handler
*       pedup(ch)               user-defined pedal up handler
*       touchchange(ch, val)    user-defined aftertouch handler
*       app_syntax              string defining extra command line options
*
*  EXPORTS:
*       
*       cause(delay, routine, p1, p2, ..., p8)
*       moxcdone -- set to TRUE to quit
*       eventtime -- ideallized current time
*
*****************************************************************************/

#define SAFEMOXC TRUE
#define BREAKKEY 0x03

int moxcdone;   /* flag to halt execution */
time_type eventtime;    /* time of current call -- used to avoid        */
                        /* timing errors due to finite execution speed  */
time_type virttime;     /* virtual time of current call */
timebase_type timebase; /* time base of current call */
int mididecode = TRUE;  /* whether to decode messages or just call midievent */

int debug = FALSE;
int moxcdebug = FALSE;
time_type next_wakeup;
timebase_type default_base;

#ifdef AMIGA
int pub_port_signal;
struct MsgPort pub_port;
#endif

/*****************************************************************************
*       Routines local to this module
*****************************************************************************/

private void callrun();
private void decode();
private void moxcterm();

/****************************************************************************
*                           callallcancel
* Inputs:
*       timebase_queue
* Effect: 
*       return all calls to free list
* Implementation:
*             If timebase_queue is not empty, there's a pending call.  Remove the call
*             (not necessarily the timebase) and repeat.
****************************************************************************/

void callallcancel()
{
    if (moxcdebug) gprintf(GDEBUG, "cancel all calls\n");
    while (timebase_queue) {
        timebase = timebase_queue;
        timebase_queue = timebase->next;
        while (timebase->heap_size > 0) {
            call_free(remove_call(timebase));
        }
        insert_base(timebase);
    }
}

/* catchup -- bring current timebase up to date by running its calls */
/**/
void catchup()
{
    register call_type call;
    /* Remember where we're going in virtual time because setting the
     * rate will also modify timebase->virt_base.  We don't want catchup
     * to stop short:
     */
    time_type target_time = timebase->virt_base;
    /* remember timebase here because it's possible that a call will do
     * a timebase_use() and change it:
     */
    register timebase_type my_base = timebase;

    while (my_base->heap_size != 0 &&
           (my_base->heap[1]->u.e.time < target_time)) {
        /* eventtime is the real time at which something was scheduled */
        eventtime = (my_base->next_time) >> 8;
        call = remove_call(my_base);
        virttime = call->u.e.time;
        (*(call->u.e.routine))(CALLARGS(call));
        call_free(call);
    }
    /* now that we've possibly pulled events out of the timebase, adjust
     * the position in the timebase queue (and possibly remove it).
     */
    remove_base(my_base);
    insert_base(my_base);
}



/****************************************************************************
*                               cause
* Inputs:
*       delay_type (long) delay: time before this call should occur
*       int (*routine)(): routine that implements the call
*       int p1 through p8: parameters to pass to routine
* Effect: 
*       builds a call and puts it in pending queue for later scheduling
****************************************************************************/

#ifndef DOTS_FOR_ARGS
void cause(delay, routine, p)
    delay_type  delay;
    int (*routine)();
    call_args_node p;
#else
#include <stdarg.h>

void cause(delay_type delay, ...)
/* note: the routine parameter is not checked because any routine type can
   be passed as a parameter, but in the call struct it's an int (*)()
 */
#endif
{
    register call_type call = call_alloc();
#ifdef DOTS_FOR_ARGS
    va_list xp;
#endif

    if (!call) {
        gprintf(ERROR, "cause: out of memory\n");
        EXIT(1);
    }

#ifdef DOTS_FOR_ARGS
    call->u.e.time = virttime + delay;
    call->u.e.priority = 128; /* default priority */
    va_start(xp, delay);
    call->u.e.routine = (int (*)()) va_arg(xp, long *);
    call->u.e.p = va_arg(xp, call_args_node);
    va_end(xp);
#else
    call->u.e.time = virttime + delay;
    call->u.e.priority = 128; /* default priority */
    call->u.e.routine = routine;
    call->u.e.p = p;
#endif
#ifdef SAFEMOXC
    if (call->u.e.routine == 0) {
        gprintf(ERROR,"cause called with NULL routine\n");
        EXIT(1);
#ifndef DOS     /* IBM allows odd addresses */
    } else if (((long) call->u.e.routine) & 1) {
        gprintf(ERROR, "cause called with bad routine address: 0x%lx\n",
                call->u.e.routine);
        EXIT(1);
#endif
    }
#endif
    /* put call in default queue */
    callinsert(timebase, call);
    if (moxcdebug) {
        gprintf(GDEBUG,"(cause) call is pending on timebase 0x%x:\n", timebase);
        callshow(call);
    }
}

/****************************************************************************
*                               causepri
* Inputs:
*       int delay: time before this call should occur
*       int pri: priority, lowest priority goes first
*       int (*routine)(): routine that implements the call
*       int p1 through p8: parameters to pass to routine
* Effect: 
*       builds a call and puts it in pending queue for later scheduling
****************************************************************************/

#ifndef DOTS_FOR_ARGS
void causepri(delay, pri, routine, p)
    delay_type  delay;
    int pri;
    int (*routine)();
    call_args_node p;
#else
/* already included stdarg.h */

void causepri(delay_type delay, int pri, ...)
#endif
{
    register call_type call = call_alloc();
#ifdef DOTS_FOR_ARGS
    va_list xp;
#endif

    if (!call) {
        gprintf(ERROR, "cause: out of memory\n");
        EXIT(1);
    }

#ifdef DOTS_FOR_ARGS
    call->u.e.time = virttime + delay;
    call->u.e.priority = pri; /* default priority */
    va_start(xp, pri);
    call->u.e.routine = (int (*)()) va_arg(xp, long *);
    call->u.e.p = va_arg(xp, call_args_node);
    va_end(xp);
#else
    call->u.e.time = virttime + delay;
    call->u.e.priority = pri; /* default priority */
    call->u.e.routine = routine;
    call->u.e.p = p;
#endif
#ifdef SAFEMOXC
    if (call->u.e.routine == 0) {
        gprintf(ERROR,"cause called with NULL routine\n");
        EXIT(1);
#ifndef DOS     /* IBM allows odd addresses */
    } else if (((long) call->u.e.routine) & 1) {
        gprintf(ERROR, "causepri called with bad routine address: 0x%lx\n",
                call->u.e.routine);
        EXIT(1);
#endif
    }
#endif
    /* put call in default queue */
    callinsert(timebase, call);
    if (moxcdebug) {
        gprintf(GDEBUG,"(cause) call is pending:");
        callshow(call);
    }
}

/****************************************************************************
*                               callrun
* Inputs:
*       call_type call: the call to execute
* Effect: 
*       executes the previously scheduled call call and deallocates it
****************************************************************************/

private void callrun()
{
    call_type call;
    if (moxcdebug) {
        gprintf(GDEBUG,"(callrun) running a call: \n");
    }
    /* remove from head of queue */
    while (!timebase_queue) gprintf(TRANS, "callrun fatal error\n");
    timebase = timebase_queue;
    timebase_queue = timebase->next;

    if (debug) gprintf(TRANS, "callrun time %ld\n", timebase->next_time);
    eventtime = (timebase->next_time) >> 8; /* real time of the call */

    /* remove first call from timebase */
    call = remove_call(timebase);
    if (debug) gprintf(TRANS, "callrun call %lx\n", (ulong)call);
    insert_base(timebase);
    virttime = call->u.e.time;          /* virtual time of the call */
    if (moxcdebug) callshow(call);
    (*(call->u.e.routine))(CALLARGS(call));
    call_free(call);
}

/****************************************************************************
*                                 m_restuntil
* Inputs:
*       int time: call time to rest until
* Effect: 
*       Waits until the specified time has been reached (absolute time).
*       Other "caused" calls will take place during the rest provided
*       this routine is called from "mainscore" (see m_rest description).
****************************************************************************/
void m_restuntil(time)
  time_type time;
{
    time = virt_to_real(timebase, time);
    while(time > gettime()) {
        moxcwait(time);
    }
}


/****************************************************************************
*                                   m_rest
* Inputs:
*       int time: Amount of time to rest
* Effect: 
*       Waits until the amount of time specified has lapsed
* Assumes:
*       Must not be called from a "caused" routine.  Must only be called
*       from "mainscore" or a routine called directly or indirectly from
*       "mainscore" without using "cause".
****************************************************************************/

void m_rest(time)
  time_type time;
{
    m_restuntil(time + real_to_virt(timebase, gettime()));      
}

/****************************************************************************
*                               moxcinit
* Inputs:
*       int argc: number of command line arguments
*       char * argv: command line argument array
* Effect: initializes moxc system
****************************************************************************/

boolean moxcinit(argc, argv)
    int argc;
    char * argv[];
{
    meminit();
    io_init();
#ifdef AMIGA
    pub_port_signal = AllocSignal(-1L);
    pub_port.mp_Node.ln_Type = NT_MSGPORT;
    pub_port.mp_SigBit = pub_port_signal;
    pub_port.mp_SigTask = FindTask(0L);
    pub_port.mp_Flags = PA_SIGNAL;
    pub_port.mp_Node.ln_Name = "CMTcmdport";
    pub_port.mp_MsgList.lh_Head =
        (struct Node *)&pub_port.mp_MsgList.lh_Tail;
    pub_port.mp_MsgList.lh_TailPred =
        (struct Node *)&pub_port.mp_MsgList.lh_Head;
    event_mask |= (1L << pub_port_signal);
    AddPort(&pub_port);
#endif
    cu_register((cu_fn_type) moxcterm, NULL);
    cl_syntax(midifns_syntax);
    cl_syntax("debug<s>Enable verbose debugging;\
        moxc<s>Enable moxc debug mode;");
    cl_syntax(app_syntax);

    if (!cl_init(argv, argc)) {
        /* make sure user gets to read the error message(s): */
        gprintf(TRANS, "Type anything to exit...");
#ifdef  DOS
        wait_ascii();
#else
        ggetchar();
#endif
        return FALSE;
    }
    debug = cl_switch("debug");
    moxcdebug = cl_switch("moxc");
    timebase = default_base = timebase_create(100);
    default_base->rate = 2560L;

    eventtime = 0L;
    next_wakeup = MAXTIME;
    musicinit();
#ifdef POSICIONADOR_3D
    ptInit();
#endif
    moxcdone = 0;
    return TRUE;
}


/****************************************************************************
*                               moxcwait
* Input:
*       -1 => wait for next keyboard or midi event or queued event
*       0 => don't wait
*       T => wait up to T for next keyboard or midi event or queued event
*               (this is used by m_restuntil)
* Assume: there is work to do (npending > 0 || evqueue) ??
* Effect: dispatch on user inputs, cause calls
****************************************************************************/

void moxcwait(dateoftimeout)
  time_type dateoftimeout;
{
    time_type maxtime = dateoftimeout;

    if (timebase_queue) {
        if ((timebase_queue->next_time >> 8) < maxtime) 
            maxtime = (timebase_queue->next_time) >> 8;
    }
    eventwait(maxtime);
    decode();
}
   

/****************************************************************************
*                               decode
* Effect: dispatch on user inputs, cause calls
****************************************************************************/

private void decode()
{
    /* It is important that midi_data is on a word boundary because we
        copy to it by doing a word transfer.
     */
    byte midi_data[4];
    time_type now;
    byte code;
    char k;
#ifdef AMIGA
    struct cmd_msg *cmd;
#endif

    now = gettime();
    timebase = default_base;
    eventtime = now;
    virttime = 0L;

/*   gprintf(GDEBUG, "decode at time %ld\n", now); */

/**********************************************
* poll for and decode midi keyboard input 
***********************************************/

    while (getbuf(FALSE, midi_data)) {
        /* only divide if necessary, divides take 100us on 8MHz 68000: */
        if (virttime == 0)
            virttime = real_to_virt(default_base, now);

        /* short-circuit midi decoding */
        if (!mididecode) {
            midievent(midi_data);
            continue;
        }

        code = midi_data[0] & MIDI_CODE_MASK;
        if (code == MIDI_ON_NOTE) {
            if (midi_data[2] == 0) {    /* velocity 0 -> note off */
                 keyup(1+(midi_data[0] & MIDI_CHN_MASK), midi_data[1]);
            } else {
                keydown((midi_data[0] & MIDI_CHN_MASK)+1,
                        midi_data[1], midi_data[2]);
            }
        } else if (code == MIDI_OFF_NOTE) {
            keyup((midi_data[0] & MIDI_CHN_MASK)+1, midi_data[1]);
        } else if (code == MIDI_TOUCH) {
            touchchange((midi_data[0] & MIDI_CHN_MASK)+1,midi_data[1]);
        } else if (code == MIDI_BEND) {
            bendchange((midi_data[0] & MIDI_CHN_MASK)+1,
                        midi_data[1] + (midi_data[2] << 7));
        } else if (code == MIDI_CTRL && midi_data[1] == SUSTAIN) {
            if (midi_data[2] == 0) pedup((midi_data[0] & MIDI_CHN_MASK) + 1);
            else peddown((midi_data[0] & MIDI_CHN_MASK) + 1);
        } else if (code == MIDI_CTRL) {
            ctrlchange((midi_data[0] & MIDI_CHN_MASK) + 1,
                        midi_data[1], midi_data[2]);
        } else if (code == MIDI_CH_PROGRAM) {
            prgmchange((midi_data[0] & MIDI_CHN_MASK) + 1, midi_data[1] + 1);
/* think C midi driver doesn't handle sysex the way the Amiga drivers do (yet) */
#ifndef MACINTOSH
        } else if (code == MIDI_SYSEX) {
            sysex();
#endif
        }
    }

/**********************************************
* poll for ASCII keyboard input 
***********************************************/
    while (get_ascii(&k)) {
        virttime = real_to_virt(default_base, now);
        asciievent(k);
        /* if user doesn't handle abort char in asciievent,
           we should exit now to avoid an infinite loop */
        if (abort_flag) EXIT(1);
    }

#ifdef POSICIONADOR_3D
/**********************************************
* poll for posicionador tridimensionale input
**********************************************/
    {
         pt_value pt_data;
         while (ptGetValue(&pt_data)) {
             /* only divide if necessary, divides take 100us on 8MHz 68000: */
             if (virttime == 0)
                 virttime = real_to_virt(default_base, now);
             ptevent(&pt_data);
        }
    }
#endif /* POSICIONADOR_3D */

#ifdef AMIGA
/**********************************************
* poll for proportional controller port 
**********************************************/
    if (prop_1_events) {
        int events;

        Disable();
            events = prop_1_events;
            prop_1_events = 0;
        Enable();
        
        if (events & BUTTON_1_RIGHT_CHANGE)
            buttonchange(3, prop_1_right_button);
        if (events & BUTTON_1_LEFT_CHANGE)
            buttonchange(2, prop_1_left_button);
        if (events & PROP_1_LEFT_CHANGE)
            propchange(2, prop_1_left_data);
        if (events & PROP_1_RIGHT_CHANGE)
            propchange(3, prop_1_right_data);
    }

/**********************************************
* poll for input from public command port
***********************************************/

    while (cmd = (struct cmd_msg *) GetMsg(&pub_port)) {
        struct symb_descr *desc = &HASHENTRY(lookup(cmd->symbol_name));
/*      gprintf(TRANS, "got %lx (%s) from pub_port\n", cmd, cmd->symbol_name); */
        virttime = real_to_virt(default_base, now);
        if (!desc) {
            gprintf(TRANS, "Error, symbol %s undefined.\n", cmd->symbol_name);
        } else if (desc->symb_type != cmd->symb_type) {
             gprintf(TRANS, "Error, wrong type for symbol %s\n",
                cmd->symbol_name); 
        } else if (cmd->symb_type == fn_symb_type) {
/*          gprintf(TRANS, "Calling routine\n"); */
            (*(desc->ptr.routine))(
             (int) cmd->the_args[0], (int) cmd->the_args[1],
             (int) cmd->the_args[2], (int) cmd->the_args[3],
             (int) cmd->the_args[4], (int) cmd->the_args[5],
             (int) cmd->the_args[6], (int) cmd->the_args[7]
            );
        } else if (cmd->symb_type == var_symb_type) {
            *(desc->ptr.intptr) = (int) cmd->the_args[0];
        } else if (cmd->symb_type == vec_symb_type) {
            if (cmd->the_args[0] >= desc->size) {
                gprintf(TRANS, "Error: Vector %s is of size %d\n",
                        cmd->symbol_name, desc->size);
            } else {
                (desc->ptr.intptr)[cmd->the_args[0]] = cmd->the_args[1];
/*              gprintf(TRANS, "vec: setting %lx\n",
                        &(desc->ptr.intptr)[cmd->the_args[0]]); */
            }
        } else gprintf(TRANS, "Symbol Type Error\n");
        ReplyMsg(&(cmd->msg));
    }
#endif
/**********************************************
* poll for next call in queue
***********************************************/
    now = (now + 1) << 8;  /* shift because next_time is also scaled,
                            * add 256 because next_time has added priority */
    if (debug)
        gprintf(TRANS, "now %ld next_time %ld\n",
                now, (timebase_queue ? timebase_queue->next_time : 1234));
    /* give pending events priority, but every 100 events, loop to allow
        input processing (user may want to give a "quit" command) */
    for (k = 0; 
          k < 100 && timebase_queue && (now > timebase_queue->next_time);
          k++) {
        callrun();
    }
/*******************
* flush text output 
********************/
#ifdef MACINTOSH_OR_UNIX
    gflush();
#endif
}


/****************************************************************************
*                               quit
* Effect: tells moxc to shut down
****************************************************************************/

void quit()
{
    moxcdone = TRUE;
}

/* moxcrun -- schedule events until done */
/**/
void moxcrun()
{
    moxcdone = FALSE;
    while (!moxcdone && !abort_flag) {          /* test for finish */
        if (!timebase_queue) moxcdone = TRUE;
        else moxcwait(MAXTIME);         /* do work */
    }
}

/* moxcterm -- clean up after moxcinit */
/**/
private void moxcterm()
{
#ifdef AMIGA
    FreeSignal((long) pub_port_signal);
    RemPort(&pub_port);
#endif
}
