// **********************************************
// File: MIDI.CPP
// MIDI file creation functions

#include "muzika.h"
#include <alloc.h>
#include <dir.h>
#include <fstream.h>
#include <io.h>
#include <string.h>
#include <values.h>

// **********************************************
// MIDIEvent is a class type for event lists
// used in MIDISecondPass.

class MIDIEvent : public Object {
  public:
    unsigned long delta;
    unsigned char freq;
    enum EVENT {PAUSE, NOTEOFF} code;

    MIDIEvent() {}
    MIDIEvent(unsigned long d, unsigned char f, enum EVENT c)
      {delta = d; freq = f; code = c;}
    virtual classType isA() const {return 0;}
    virtual char *nameOf() const {return NULL;}
    virtual hashValueType hashValue() const {return 0;}
    virtual int isEqual(const Object &) const {return 0;}
    virtual void printOn(ostream &) const {}
};

// **********************************************
// MIDIStaff converts a multiple staff to a temporary
// MIDI-like file during the first pass of the translation.
// The temporary file contains information about note-on
// and note-off events, to be converted to a final format
// during the second pass.

void MIDIStaff(ostream &out, int baseStaff, int multiplicity)
{
  // Allocate a few lists for use in the sequel
  int *duration = (int *) malloc(multiplicity*sizeof(int));
  int *index = (int *) malloc(multiplicity*sizeof(int));
  BOOL *staffEnd = (int *) malloc(multiplicity*sizeof(BOOL));
  memset(duration, 0, multiplicity*sizeof(int));
  memset(index, 0, multiplicity*sizeof(int));
  memset(staffEnd, 0, multiplicity*sizeof(BOOL));
  BOOL multipleEnd;

  // Loop as long as the end has not been reached on all single staves
  do {
    // Find the minimum next object duration among the staves
    int minIndex = FindMinDuration(duration, multiplicity);
    int minDuration = duration[minIndex];
    int partIndex = 0, staffIndex = 0, anIndex = 0;
    multipleEnd = TRUE;

    // Subtract the minimum duration from all staves
    while (partIndex < melody.part.number()) {
      Part &p = *((Part *) &melody.part[partIndex]);
      Staff &s = *((Staff *) &p.staff[baseStaff*p.multiplicity()+staffIndex]);

      if (!(duration[anIndex] -= minDuration)) {
        // The duration has reached zero as a result of the subtraction:
        // place the next object in the temporary event file
        int currX = ((PointObject *) &s.pointObject[index[anIndex]])->X();
        while (!(staffEnd[anIndex] =
          index[anIndex] == s.pointObject.number())) {
          PointObject &obj =
            *((PointObject *) &s.pointObject[index[anIndex]]);

          if (obj.X() == currX) {
            if (duration[anIndex] < obj.Duration())
              duration[anIndex] = obj.Duration();

            // Use the object's MIDIPlay virtual function to write
            // the object information to the file
            obj.MIDIPlay(out, partIndex);
            ++index[anIndex];
          }
          else break;
        }
      }

      // Check whether the end has been reached on all single staves
      multipleEnd = multipleEnd && staffEnd[anIndex];
      ++anIndex;
      ++staffIndex;
      if (staffIndex >= p.staff.number()) {
        ++partIndex;
        staffIndex = 0;
      }
    }
  } while (!multipleEnd);

  // Free the temporary lists on the heap
  free(staffEnd);
  free(index);
  free(duration);
}

// **********************************************
// MIDIFirstPass performs the first pass of the
// melody-to-MIDI translation process, creating a temporary
// MIDI-like file with information about the played notes.

void MIDIFirstPass(ofstream &out)
{
  // Calculate the total number of staves
  int scoreMultiplicity = 0;
  scoreStaves = 0;
  for (int index = 0; index < melody.part.number(); ++index) {
    Part &p = *((Part *) &melody.part[index]);
    scoreMultiplicity += p.multiplicity();
    if (p.staff.number()/p.multiplicity() > scoreStaves)
      scoreStaves = p.staff.number()/p.multiplicity();
  }

  // Call MIDIStaff once per every multiple staff
  for (int scoreIndex = 0; scoreIndex < scoreStaves; ++scoreIndex)
    MIDIStaff(out, scoreIndex, scoreMultiplicity);
}

// **********************************************
// InsertEvent inserts a MIDI event in an IndexedList object
// during the second pass of the translation. The events
// are held in a delta list, with the delta field holding the
// times between events.

void InsertEvent(IndexedList &list, enum MIDIEvent::EVENT event,
  unsigned char freq, unsigned long delay)
{
  MIDIEvent *e;

  // Find the index to insert the event at, reducing the delay in the way
  for (int i = 0;
    i < list.number() && (e = ((MIDIEvent *) &list[i]))->delta < delay;
    ++i)
    delay -= e->delta;

  // Subtract the new event's delay from the next event
  if (i < list.number())
    e->delta -= delay;

  // Insert the new event in the list
  list.insertAt(*new MIDIEvent(delay, freq, event), i);
}

// **********************************************
// WriteVariableLength converts a long-integer duration
// to the variable-length, MIDI-file format.

int WriteVariableLength(ostream &out, unsigned long duration)
{
  out.put((char) duration);
  return 1;
}

// **********************************************
// MIDISecondPass converts the temporary file, created by
// MIDIFirstPass, to the final-version MIDI file. The main job
// of MIDISecondPass is to insert note-off events at the right places.
// Whenever a note-on event is encountered, a note-off event is
// inserted in the appropriate event list, and is eventually
// written to the MIDI file.

void MIDISecondPass(ifstream &in, ostream &out)
{
  const int n = melody.part.number();
  IndexedList event[16];
  unsigned long length = 0;
  unsigned char code;
  unsigned long duration;
  BOOL allEmpty = TRUE, oneEmpty;

  // Write the MIDI file header
  out.write("MThd\0\0\0\6\0\0\0\1\0\x0C", 14);
  out.write("MTrk\0\0\0\0", 8);
  out.write("\0\xFF\x58\4\4\2\x18\8", 8);
  out.write("\0\xFF\x51\3\8\x2C\xA2", 7);
  length += 15;
  for (int index = 0; index < n; ++index) {
    out.put(0);
    out.put(0xC0+index);
    out.write("\x58\5", 2);
    length += 4;
  }

  // Loop as long as the file and the event lists are not over with
  while (!in.eof() || !allEmpty) {
    // Check if there are empty lists
    oneEmpty = FALSE;
    for (index = 0; index < n; ++index)
      oneEmpty = oneEmpty || (event[index].number() == 0);

    // If at least one empty list, try to read another event from the file
    if (oneEmpty && !in.eof()) {
      if ((code = in.get()) >= 0x80 && code <= 0x8F) {
        // The code is a Pause of some kind:
        // Put a PAUSE event at end of list
        in.read((char *) &duration, sizeof(int));
        InsertEvent(event[code-0x80], MIDIEvent::PAUSE, 0, duration);
      }
      else if (code >= 0x90 && code <= 0x9F) {
        // The code is a Note of some kind:
        // Put a "note on" event in file
        // and insert a NOTEOFF event at end of list
        in.read((char *) &duration, sizeof(int));
        out.put(0);
        out.put(code);
        unsigned char freq;
        out.put(freq = in.get());
        out.put(in.get());
        length += 4;
        InsertEvent(event[code-0x90], MIDIEvent::NOTEOFF, freq, duration);
      }
      else if (code == 0xFF)
        // Code is a meta-event:
        // do whatever is supported about meta-events (currently nothing)
        ;
    }
    else {
      // No empty lists:
      // Find the list with minimum delta-time
      // in order to write its event into the MIDI file
      unsigned long minDelta = MAXLONG;
      for (index = 0; index < n; ++index)
        if (event[index].number())
          if (((MIDIEvent *) &event[index][0])->delta < minDelta)
            minDelta = ((MIDIEvent *) &event[index][0])->delta;
      duration = minDelta;

      // Subtract the delta-time from all lists
      for (index = 0; index < n; ++index)
        if (event[index].number())
          if (!(((MIDIEvent *) &event[index][0])->delta -= minDelta)) {
            // Output the event to file and accumulate length
            MIDIEvent &e = *((MIDIEvent *) &event[index][0]);
            length += WriteVariableLength(out, duration)+3;
            out.put(0x80+index);
            out.put(e.freq);
            out.put(0x7F);
            duration = 0;
            event[index].destroyAt(0);
          }
    }

    // See if all lists have been processed
    allEmpty = TRUE;
    for (index = 0; index < n; ++index)
      allEmpty = allEmpty && (event[index].number() == 0);
  }

  // Write a Track End event
  out.write("\0\xFF\x2F\0", 4);

  // Update the track length information
  length += 4;
  out.seekp(18);
  char revlength[sizeof(length)];
  memcpy(revlength, &length, sizeof(length));
  for (index = sizeof(length)-1; index >= 0; --index)
    out.put(revlength[index]);
}

// **********************************************
// CreateMIDIFile is called from the outside to create a MIDI file.
// It calls MIDIFirstPass with a temporary file and then
// MIDISecondPass to translate the temporary to a final file.

void CreateMIDIFile(ostream &out)
{
  char path[MAXPATH];
  strcpy(path, ".\\");
  ofstream temp(creattemp(path, 0));
  MIDIFirstPass(temp);
  temp.close();
  ifstream temp1(path, ios::binary | ios::in);
  MIDISecondPass(temp1, out);
  temp1.close();
  unlink(path);
}
