/**************************************************************************

    midfile.cpp - function which reads a midi file,and creates the track classes
    This file is part of LibKMid 0.9.5
    Copyright (C) 1997,98,99,2000  Antonio Larrosa Jimenez
    LibKMid's homepage : http://www.arrakis.es/~rlarrosa/libtdemid.html

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.
 
    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Library General Public License for more details.
 
    You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to
    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA 02110-1301, USA.

    Send comments and bug fixes to Antonio Larrosa <larrosa@kde.org>

***************************************************************************/
#include "midfile.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "sndcard.h"
#include "midispec.h"
#include "mt32togm.h"
#include "sys/stat.h"
#include <config.h>

#include <kprocess.h>
#include <tqfile.h>

int fsearch(FILE *fh,const char *text,long *ptr);

/* This function gives the metronome tempo, from a tempo data as found in
   a midi file */
double tempoToMetronomeTempo(ulong x)
{
  return 60/((double)x/1000000);
}

double metronomeTempoToTempo(ulong x)
{
  return ((double)60*x)/1000000;
}

int uncompressFile(const char *gzname, char *tmpname)
  // Returns 0 if OK, 1 if error (tmpname not set)
{
  TQString cmd("gzip -dc " + TDEProcess::quote(gzname));
  FILE *infile = popen( TQFile::encodeName(cmd).data(), "r");
  if (infile==NULL) {
    fprintf(stderr,"ERROR : popen failed : %s\n",TQFile::encodeName(cmd).data());
    return 1;
  }
  strcpy(tmpname, "/tmp/KMid.XXXXXXXXXX");
  int fd = mkstemp(tmpname);
  if (fd == -1)
  {
    pclose(infile);
    return 1;
  }
  FILE *outfile= fdopen(fd,"wb");
  if (outfile==NULL)
  {
    pclose(infile);
    return 1;
  }
  int n=getc(infile);
  if (n==EOF) 
  {
    pclose(infile);
    fclose(outfile);
    unlink(tmpname);
    return 1;
  }
  fputc(n,outfile);
  int buf[BUFSIZ];
  n = fread(buf, 1, BUFSIZ, infile);
  while (n>0)
  {
    fwrite(buf, 1, n, outfile);
    n = fread(buf, 1, BUFSIZ, infile);
  }

  pclose(infile);

  //if (pclose(infile) != 0) fprintf(stderr,"Error : pclose failed\n");
  // Is it right for pclose to always fail ?

  fclose(outfile);
  return 0;
}

MidiTrack **readMidiFile( const char *name, MidiFileInfo *info, int &ok)
{
  ok=1;
  MidiTrack **tracks;

  struct stat buf;
  if (stat(name,&buf) || !S_ISREG(buf.st_mode))
  {
    fprintf(stderr,"ERROR: %s is not a regular file\n",name);
    ok=-6;
    return NULL;
  }

  FILE *fh=fopen(name,"rb");
  if (fh==NULL) 
  {
    fprintf(stderr,"ERROR: Can't open file %s\n",name);
    ok=-1;
    return NULL;
  }
  char text[4];
  text[0] = 0;
  fread(text,1,4,fh);
  if ((strncmp(text,"MThd",4)!=0)&&(strcmp(&name[strlen(name)-3],".gz")==0))
  {	
    fclose(fh);
    char tempname[200];
    fprintf(stderr,"Trying to open zipped midi file...\n");
    if (uncompressFile(name,tempname)!=0)
    {
      fprintf(stderr,"ERROR: %s is not a (zipped) midi file\n",name);
      ok=-2;
      return NULL;
    }
    fh=fopen(tempname,"rb");
    fread(text,1,4,fh);
    unlink(tempname);
  }

  if (strncmp(text,"MThd",4)!=0)
  {
    fseek(fh,0,SEEK_SET);
    long pos;
    if (fsearch(fh,"MThd",&pos)==0)
    {	
      fclose(fh);
      fprintf(stderr,"ERROR: %s is not a midi file.\n",name);
      ok=-2;
      return NULL;
    }
    fseek(fh,pos,SEEK_SET);
    fread(text,1,4,fh);
  }
  long header_size=readLong(fh);
  info->format=readShort(fh);
  info->ntracks=readShort(fh);
  info->ticksPerCuarterNote=readShort(fh);
  if (info->ticksPerCuarterNote<0)
  {
    fprintf(stderr,"ERROR: Ticks per cuarter note is negative !\n");
    fprintf(stderr,"Please report this error to : larrosa@kde.org\n");
    fclose(fh);
    ok=-3;
    return NULL;
  }
  if (header_size>6) fseek(fh,header_size-6,SEEK_CUR);
  tracks=new MidiTrack*[info->ntracks];
  if (tracks==NULL)
  {
    fprintf(stderr,"ERROR: Not enough memory\n");
    fclose(fh);
    ok=-4;
    return NULL;
  }
  int i=0;
  while (i<info->ntracks)
  {
    fread(text,1,4,fh);
    if (strncmp(text,"MTrk",4)!=0)
    {
      fprintf(stderr,"ERROR: Not a well built midi file\n");
      fprintf(stderr,"%s",text);
      fclose(fh);
      ok=-5;
      return NULL;
    }
    tracks[i]=new MidiTrack(fh,info->ticksPerCuarterNote,i);
    if (tracks[i]==NULL)
    {
      fprintf(stderr,"ERROR: Not enough memory");
      fclose(fh);
      ok=-4;
      return NULL;
    }
    i++;
  }

  fclose(fh);

  return tracks;

}

void parseInfoData(MidiFileInfo *info,MidiTrack **tracks,float ratioTempo)
{

  info->ticksTotal=0;
  info->millisecsTotal=0.0;
  info->ticksPlayed=0;
  int i;
  for (i=0;i<256;i++)
  {
    info->patchesUsed[i]=0;
  }

  int parsing=1;
  int trk,minTrk;
  ulong tempo=(ulong)(500000 * ratioTempo);

#ifdef MIDFILEDEBUG
  printf("Parsing 1 ...\n");
#endif

  int pgminchannel[16];
  for (i=0;i<16;i++) 
  {
    pgminchannel[i]=0;
  }

  int j;
  for (i=0;i<info->ntracks;i++)
  {
    tracks[i]->init();
    tracks[i]->changeTempo(tempo);
  }
  double prevms=0;
  double minTime=0;
  double maxTime;
  MidiEvent *ev=new MidiEvent;
  while (parsing)
  {
    prevms=minTime;
    trk=0;
    minTrk=0;
    maxTime=minTime + 2 * 60000L;
    minTime=maxTime;
    while (trk<info->ntracks)
    {
      if (tracks[trk]->absMsOfNextEvent()<minTime)
      {
	minTrk=trk;
	minTime=tracks[minTrk]->absMsOfNextEvent();
      }
      trk++;
    }
    if ((minTime==maxTime))
    {
      parsing=0;
#ifdef MIDFILEDEBUG
      printf("END of parsing\n");
#endif
    }
    else
    {
      trk=0;
      while (trk<info->ntracks)
      {
	tracks[trk]->currentMs(minTime);
	trk++;
      }
    }
    trk=minTrk;
    tracks[trk]->readEvent(ev);

    switch (ev->command)
    {
      case (MIDI_NOTEON) : 
	if (ev->chn!=PERCUSSION_CHANNEL)
	  info->patchesUsed[pgminchannel[ev->chn]]++;
	else
	  info->patchesUsed[ev->note+128]++;
	break;
      case (MIDI_PGM_CHANGE) :
	pgminchannel[ev->chn]=(ev->patch);
	break;
      case (MIDI_SYSTEM_PREFIX) :
	if (((ev->command|ev->chn)==META_EVENT)&&(ev->d1==ME_SET_TEMPO))
	{
	  tempo=(ulong)(((ev->data[0]<<16)|(ev->data[1]<<8)|(ev->data[2])) * ratioTempo);
	  for (j=0;j<info->ntracks;j++)
	  {
	    tracks[j]->changeTempo(tempo);
	  }
	}
	break;
    }
  }

  delete ev;
  info->millisecsTotal=prevms;

  for (i=0;i<info->ntracks;i++)
  {
    tracks[i]->init();
  }

#ifdef MIDFILEDEBUG
  printf("info.ticksTotal = %ld \n",info->ticksTotal);
  printf("info.ticksPlayed= %ld \n",info->ticksPlayed);
  printf("info.millisecsTotal  = %g \n",info->millisecsTotal);
  printf("info.TicksPerCN = %d \n",info->ticksPerCuarterNote);
#endif

}


void parsePatchesUsed(MidiTrack **tracks,MidiFileInfo *info,int gm)
{
  int i;
  for (i=0;i<256;i++)
  {
    info->patchesUsed[i]=0;
  }
  int parsing=1;
  int trk,minTrk;
  ulong tempo=500000;

#ifdef MIDFILEDEBUG
  printf("Parsing for patches ...\n");
#endif

  int j;
  for (i=0;i<info->ntracks;i++)
  {
    tracks[i]->init();
  }
  double prevms=0;
  double minTime=0;
  double maxTime;
  ulong tmp;
  MidiEvent *ev=new MidiEvent;
  int pgminchannel[16];
  for (i=0;i<16;i++)
  {
    pgminchannel[i]=0;
  }

  while (parsing)
  {
    prevms=minTime;
    trk=0;
    minTrk=0;
    maxTime=minTime + 2 * 60000L;
    minTime=maxTime;
    while (trk<info->ntracks)
    {
      if (tracks[trk]->absMsOfNextEvent()<minTime)
      {
	minTrk=trk;
	minTime=tracks[minTrk]->absMsOfNextEvent();
      }
      trk++;
    }
    if ((minTime==maxTime))
    {
      parsing=0;
#ifdef MIDFILEDEBUG
      printf("END of parsing for patches\n");
#endif
    }
    else
    {
      trk=0;
      while (trk<info->ntracks)
      {
	tracks[trk]->currentMs(minTime);
	trk++;
      }
    }
    trk=minTrk;
    tracks[trk]->readEvent(ev);
    switch (ev->command)
    {
      case (MIDI_NOTEON) : 
	if (ev->chn!=PERCUSSION_CHANNEL)
	  info->patchesUsed[pgminchannel[ev->chn]]++;
	else
	  info->patchesUsed[ev->note+128]++;
	break;
      case (MIDI_PGM_CHANGE) :
	pgminchannel[ev->chn]=(gm==1)?(ev->patch):(MT32toGM[ev->patch]);
	break;
      case (MIDI_SYSTEM_PREFIX) :
	if (((ev->command|ev->chn)==META_EVENT)&&(ev->d1==ME_SET_TEMPO))
	{
	  if (tempoToMetronomeTempo(tmp=((ev->data[0]<<16)|(ev->data[1]<<8)|(ev->data[2])))>=8)
	  {
	    tempo=tmp;
	    //                   printf("setTempo %ld\n",tempo);
	    for (j=0;j<info->ntracks;j++)
	    {
	      tracks[j]->changeTempo(tempo);
	    }
	  }
	}
	break;   
    }
  }

  delete ev;

  for (i=0;i<info->ntracks;i++)
  {
    tracks[i]->init();
  }

}

int fsearch(FILE *fh,const char *text,long *ptr)
  // Search for "text" through the fh file and then returns :
  // text MUST BE smaller than 256 characters
  //   0 if not was found 
  //   1 if it was found and in ptr (if !=NULL) the position where text begins.
{
  if ((text==NULL)||(text[0]==0)) return 0;
  char buf[1024];
  char tmp[256];
  long pos;
  int l=strlen(text);
  int i,k,r;
  while (!feof(fh))
  {
    pos=ftell(fh);
    k=fread(buf,1,1024,fh);
    i=0;
    while (i<k)
    {
      if (buf[i]==text[0])
      {
	if (k-i>=l)
	  r=strncmp(text,&buf[i],l);
	else
	{
	  fseek(fh,pos+i,SEEK_SET);
	  if (fread(tmp,1,l,fh)<(uint)l) return 0;
	  fseek(fh,pos+k,SEEK_SET);
	  r=strncmp(text,tmp,l);
	}
	if (r==0)
	{
	  if (ptr!=NULL) *ptr=pos+i;
	  return 1;
	}
      }
      i++;
    }
  }
  return 0;
}
