#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>

using namespace std;

void WriteVarLen(ofstream &os, unsigned long value)
{
  unsigned long buffer;
  buffer = value & 0x7F;
  
  while ( (value >>= 7) )
  {
    buffer <<= 8;
    buffer |= ((value & 0x7F) | 0x80);
  }
  
  while (1)
  {
    os.put(buffer);
    if (buffer & 0x80)
      buffer >>= 8;
    else
      break;
  }
}

inline void WriteDeltaTime(ofstream &os, unsigned long value)
{
  WriteVarLen(os, value);
}

void WriteChunkType(ofstream &os, char c1, char c2, char c3, char c4)
{
  os.put(c1);
  os.put(c2);
  os.put(c3);
  os.put(c4);
}

void WriteUnsignedLong(ofstream &os, unsigned long ul)
{
  os.put(ul >> 24);
  os.put(ul >> 16 & 0xff);
  os.put(ul >> 8 & 0xff);
  os.put(ul & 0xff);  
}

void WriteUnsigned24BitValue(ofstream &os, unsigned long ul)
{
  os.put(ul >> 16 & 0xff);
  os.put(ul >> 8 & 0xff);
  os.put(ul & 0xff);  
}

void WriteUnsignedShort(ofstream &os, unsigned short us)
{
  os.put(us >> 8);
  os.put(us & 0xff);  
}

void WriteUnsignedChar(ofstream &os, unsigned char ub)
{
  os.put(ub);  
}

void WriteHeaderChunk(ofstream &os)
{
  const int ntrks = 2;
  const int division = 480;
    
  WriteChunkType(os, 'M', 'T', 'h', 'd');
  WriteUnsignedLong(os, 6);  // length of header data
  WriteUnsignedShort(os, 1);  // format
  WriteUnsignedShort(os, ntrks);  // ntrks
  WriteUnsignedShort(os, division);  // division
}

void WriteSequenceOrTrackName(ofstream &os, string n)
{
  if (n.length() > 256)
    throw length_error("Name of track must be less than 256 chars");
  
  WriteUnsignedChar(os, 0xff);  // meta
  WriteUnsignedChar(os, 0x03);  // time signature
  WriteUnsignedChar(os, n.length());  // length
  
  os << n;
}

void WriteTimeSignature(ofstream &os)
{
  const int timeSignatureNumerator = 4;
  const int timeSignatureDenominator = 2;  // 2^2 = 4
  const int MIDIClockPerClick = 24;
  const int ThirtySecondNotesPerQuarterNote = 8;

  WriteUnsignedChar(os, 0xff);  // meta
  WriteUnsignedChar(os, 0x58);  // time signature
  WriteUnsignedChar(os, 0x04);  // length
  
  WriteUnsignedChar(os, timeSignatureNumerator);
  WriteUnsignedChar(os, timeSignatureDenominator);
  WriteUnsignedChar(os, MIDIClockPerClick);
  WriteUnsignedChar(os, ThirtySecondNotesPerQuarterNote);
}

void WriteSetTempo(ofstream &os)
{
  const int BPM = 120;
  
  WriteUnsignedChar(os, 0xff);  // meta
  WriteUnsignedChar(os, 0x51);  // set tempo
  WriteUnsignedChar(os, 0x03);  // length
  
  WriteUnsigned24BitValue(os, 60000000 / BPM);
}

void WriteKeySignature(ofstream &os)
{
  const int sf = 2;  // D (2 sharps)
  const int mi = 0;  // major key
  
  WriteUnsignedChar(os, 0xff);  // meta
  WriteUnsignedChar(os, 0x59);  // set tempo
  WriteUnsignedChar(os, 0x02);  // length
  
  WriteUnsignedChar(os, sf);
  WriteUnsignedChar(os, mi);
}

void WriteEndOfTrack(ofstream &os)
{
  WriteUnsignedChar(os, 0xff);  // meta
  WriteUnsignedChar(os, 0x2f);  // end of track
  WriteUnsignedChar(os, 0x00);  // length
}

void WriteTempoTrackChunk(ofstream &os)
{
  WriteChunkType(os, 'M', 'T', 'r', 'k');

  ofstream::pos_type locationOfLength = os.tellp();
  WriteUnsignedLong(os, 0);  // reserve space for length

  ofstream::pos_type startOfData = os.tellp();

  WriteDeltaTime(os, 0);
  WriteSequenceOrTrackName(os, "Sequence 1");
  WriteDeltaTime(os, 0);
  WriteTimeSignature(os);
  WriteDeltaTime(os, 0);
  WriteSetTempo(os);
  WriteDeltaTime(os, 0);
  WriteKeySignature(os);
  WriteDeltaTime(os, 480);
  WriteEndOfTrack(os);
  
  ofstream::pos_type endOfData = os.tellp();
  
  unsigned long length = endOfData - startOfData;
  os.seekp(locationOfLength);
  WriteUnsignedLong(os, length);
  os.seekp(endOfData);
}

void WriteProgramChange(ofstream &os, int channel, int pc)
{
  if (!(1 <= channel && channel <= 16))
    throw range_error("channel must be between 1 and 16");
  
  if (!(1 <= pc && pc <= 256))
    throw range_error("program change must be between 1 and 256");
  
  WriteUnsignedChar(os, 0xc0 | (channel - 1));
  WriteUnsignedChar(os, pc - 1);
}

void WriteNoteOn(ofstream &os, int channel, int pitch, int velocity)
{
  if (!(1 <= channel && channel <= 16))
    throw range_error("channel must be between 1 and 16");
  
  if (!(0 <= pitch && pitch <= 127))
    throw range_error("pitch must be between 0 and 127");

  if (!(0 <= velocity && velocity <= 127))
    throw range_error("velocity must be between 0 and 127");
  
  WriteUnsignedChar(os, 0x90 | (channel - 1));
  WriteUnsignedChar(os, pitch);
  WriteUnsignedChar(os, velocity);
}

void WriteNoteOff(ofstream &os, int channel, int pitch)
{
  if (!(1 <= channel && channel <= 16))
    throw range_error("channel must be between 1 and 16");
  
  if (!(0 <= pitch && pitch <= 127))
    throw range_error("pitch must be between 0 and 127");
  
  WriteUnsignedChar(os, 0x80 | (channel - 1));
  WriteUnsignedChar(os, pitch);
  WriteUnsignedChar(os, 64);  // standard velocity for note off
}

void WriteBassTrackChunk(ofstream &os)
{
  WriteChunkType(os, 'M', 'T', 'r', 'k');

  ofstream::pos_type locationOfLength = os.tellp();
  WriteUnsignedLong(os, 0);  // reserve space for length
  
  ofstream::pos_type startOfData = os.tellp();
  
  WriteDeltaTime(os, 0);
  WriteSequenceOrTrackName(os, "Bass");
  WriteDeltaTime(os, 0);
  WriteProgramChange(os, 2, 33);
  
  WriteDeltaTime(os, 0);
  WriteNoteOn(os, 2, 36, 96);
  WriteDeltaTime(os, 480);
  WriteNoteOff(os, 2, 36);

  WriteDeltaTime(os, 0);
  WriteNoteOn(os, 2, 38, 96);
  WriteDeltaTime(os, 480);
  WriteNoteOff(os, 2, 38);
  
  WriteDeltaTime(os, 0);
  WriteNoteOn(os, 2, 40, 96);
  WriteDeltaTime(os, 480);
  WriteNoteOff(os, 2, 40);
  
  WriteDeltaTime(os, 0);
  WriteNoteOn(os, 2, 41, 96);
  WriteDeltaTime(os, 480);
  WriteNoteOff(os, 2, 41);
  
  WriteDeltaTime(os, 0);
  WriteNoteOn(os, 2, 43, 96);
  WriteDeltaTime(os, 480);
  WriteNoteOff(os, 2, 43);
  
  WriteDeltaTime(os, 0);
  WriteNoteOn(os, 2, 45, 96);
  WriteDeltaTime(os, 480);
  WriteNoteOff(os, 2, 45);
  
  WriteDeltaTime(os, 0);
  WriteNoteOn(os, 2, 47, 96);
  WriteDeltaTime(os, 480);
  WriteNoteOff(os, 2, 47);
  
  WriteDeltaTime(os, 0);
  WriteNoteOn(os, 2, 48, 96);
  WriteDeltaTime(os, 480);
  WriteNoteOff(os, 2, 48);
  
  WriteDeltaTime(os, 0);
  WriteEndOfTrack(os);
  
  ofstream::pos_type endOfData = os.tellp();
  
  unsigned long length = endOfData - startOfData;
  os.seekp(locationOfLength);
  WriteUnsignedLong(os, length);
  os.seekp(endOfData);
}

int main (int argc, char * const argv[]) {
  if (argc != 2) {
    cout << "Usage: MIDIFileWriter filename" << endl;
    return 1;
  }
  
  ofstream ofs(argv[1]);
  if (!ofs) {
    cout << "Cannot open output file \"" << argv[1] << "\"" << endl;
    return 3;
  }
  
  WriteHeaderChunk(ofs);
  WriteTempoTrackChunk(ofs);
  WriteBassTrackChunk(ofs);
  
  return 0;
}

