// Copyright (1995) by melonSoft Ralf Suckow Berlin, All Rights Reserved

// Note: sometimes we use the Class * type instead of id since
// the appkit includes the soundkit, which has different definitions
// for the activate and info methods.

#import "MLPluckMaster.h"
#import "MLParameters.h"

#define PLUCK_ML_FILE_FORMAT "%lf %lf %lf\n"
#define EXTENSION            "pluck-ml"
#define TMP_FILE             "/tmp/plucktempfile_%s.snd"

@implementation MLPluckMaster

- appDidInit:sender
{
  static char name[80];

  sprintf (name, VOICE_PRODUCER_NAME, EXTENSION);
  [[NXConnection registerRoot:self withName:name] runFromAppKit];

  // initialize self
  return self;
}

- showError:(const char *)text with:(const char *)parameter
{
  NXRunAlertPanel ("Pluck Voice",
                    text, NULL, NULL, NULL, parameter);
  return self;
}

- getPosition:(double *)position attenuation:(double *)attenuation
  durationFactor:(double *)durationFactor fromFile:(const char *)inputFile
{
  // scanning the ASCII pluck-ml file
  // its syntax is extremely simple: just three floating point numbers,
  // one for the default position, one for the default attenuation,
  // and one for the default durationFactor

  NXStream * stream;

  // default values for the default values - if the scan is unsuccessful
  *position = 0.1;
  *attenuation = 0.5;
  *durationFactor = 1;

  NX_DURING

    stream = NXMapFile (inputFile, NX_READONLY);
    if (NXScanf (stream, PLUCK_ML_FILE_FORMAT,
                 position, attenuation, durationFactor) != 3)
      [self showError:"bad content of file `%s'" with:inputFile];
    NXCloseMemory (stream, NX_FREEBUFFER);

  NX_HANDLER

    [self showError:"I/O error -- Can't read from file `%s'" with:inputFile];

  NX_ENDHANDLER

  return self;
}

- (double)getParameter:(const char *)name default:(double)dValue from:params
{
  NXAtom atomName;

  atomName = NXUniqueString (name);

  if ([params contains:atomName])
    return [params doubleValueFor:atomName];
  else
    return dValue;
}

- restrict:(double *)value toMin:(double)min max:(double)max
{
  if (*value < min)
    *value = min;
  if (*value > max)
    *value = max;
  return self;
}

- (BOOL)makeFile:(const char *)outputFile
  withFrequency:(double)frequency volume:(double)volume
  duration:(double)duration position:(double)position
  attenuation:(double)attenuation durationFactor:(double)durationFactor
{
  int              fd;
  SNDSoundStruct * soundStruct;
  int              length;
  short int      * buf;

  [self restrict:&frequency      toMin:  10 max: 2500];
  [self restrict:&volume         toMin:   0 max:    1];
  [self restrict:&duration       toMin:   0 max:   20];
  [self restrict:&position       toMin:   0 max:  0.5];
  [self restrict:&attenuation    toMin:0.46 max:  0.5];
  [self restrict:&durationFactor toMin:   0 max:  100];

  // we use level 2 here since SNDWriteHeader uses it
  fd = open (outputFile, O_CREAT | O_WRONLY | O_TRUNC, 0644);
  if (fd == -1) {
    [self showError:"I/O error -- Can't write to file `%s'" with:outputFile];
    return NO;
  }

  // see MLPluckAlgorithm.m for the following method
  length = [self calculateSamples:&buf withFrequency:frequency volume:volume
                                         duration:duration position:position
                       attenuation:attenuation durationFactor:durationFactor];

  if (!buf || !length) {
    close (fd);
    return NO;
  }

  // write header
  SNDAlloc(&soundStruct, 0, SND_FORMAT_LINEAR_16, SND_RATE_HIGH, 1, 4);
  strcpy (soundStruct->info, "mls");
  soundStruct->dataSize = length * sizeof (short int);
  SNDWriteHeader (fd, soundStruct);
  soundStruct->dataSize = 0;
  SNDFree (soundStruct);

  // write data
  SNDSwapHostToSound (buf, buf, length, 1, SND_FORMAT_LINEAR_16);
  write (fd, buf, length * sizeof (short int));
  free (buf);
  close (fd);
  return YES;
}

- (BOOL)makeFile:(const char *)outputFile
  fromVoice:(const char *)inputFile
  parameters:parameters
{
  double defaultPosition, defaultAttenuation, defaultDurationFactor;
  double position, attenuation, duration, frequency, volume, durationFactor;

  [self getPosition:&defaultPosition
        attenuation:&defaultAttenuation
        durationFactor:&defaultDurationFactor
        fromFile:inputFile];

  position       = [self getParameter:"position"
                         default:defaultPosition
                         from:parameters];

  attenuation    = [self getParameter:"attenuation"
                         default:defaultAttenuation
                         from:parameters];

  durationFactor = [self getParameter:"durationFactor"
                         default:defaultDurationFactor
                         from:parameters];

  frequency      = [self getParameter:"freq"
                         default:220
                         from:parameters];

  volume         = [self getParameter:"volume"
                         default:0.5
                         from:parameters];

  duration       = [self getParameter:"duration"
                         default:0.3
                         from:parameters];

  return [self makeFile:outputFile withFrequency:frequency
                                   volume:volume
                                   duration:duration
                                   position:position
                                   attenuation:attenuation
                                   durationFactor:durationFactor];
}

// <MLVoiceProduction> protocol

- (oneway void)voice:(const char *)documentFile
               makeFile:(const char *)outputFile
               from:(const char *)parameters
               forClient:(id <MLProductionClient>)sender
               request:(int)number;
{
  BOOL ok;
  id   params;
  BOOL flag;

  params = [[MLParameters alloc] init];

  if (![params readFromMathString:parameters syntaxOK:&flag])
    ok = NO;
  else
    ok = [self makeFile:outputFile
               fromVoice:documentFile
               parameters:params];

  [sender server:self didRequest:number success:(int)ok];
  [params free];

  // trying to free the strings -- does this always work ?
  if (documentFile)
    free ((char *)documentFile);
  if (outputFile)
    free ((char *)outputFile);
  if (parameters)
    free ((char *)parameters);
}

// support for double clicking the .pluck-ml file - opens the file in editor

- (BOOL)appAcceptsAnotherFile:sender
{
  return YES;
}

- (int)app:sender openFile:(const char *)path type:(const char *)type
{
  double position, attenuation, durationFactor;

  [self getPosition:&position
        attenuation:&attenuation
        durationFactor:&durationFactor
        fromFile:path];

  [positionField setDoubleValue:position];
  [attenuationField setDoubleValue:attenuation];
  [durationFactorField setDoubleValue:durationFactor];

  [[positionField window] makeKeyAndOrderFront:self];

//  [self perform:@selector(doOpen:) with:(id)NXUniqueString (path)
//        afterDelay:0 cancelPrevious:NO];
  return YES;
}

// - doOpen:path
// {
//   // It is a better idea to forward it to Edit even if somebody is
//   // using emacs or any other editor
//
//   [[Application workspace] openFile:(const char *)path
//                           withApplication:"Edit"];
//   return self;
// }

- saveSound:sender
{
  id savepanel;

  savepanel = [SavePanel new];
  [savepanel setRequiredFileType:"snd"];

  if ([savepanel runModal])

    [self makeFile:[savepanel filename]
          withFrequency:[frequencyField doubleValue]
          volume:[volumeField doubleValue]
          duration:[durationField doubleValue]
          position:[positionField doubleValue]
          attenuation:[attenuationField doubleValue]
          durationFactor:[durationFactorField doubleValue]];

  return self;
}

- saveSettings:sender
{
  id         savepanel;
  NXStream * stream;

  savepanel = [SavePanel new];
  [savepanel setRequiredFileType:EXTENSION];

  if ([savepanel runModal]) {

    NX_DURING

      stream = NXOpenMemory (NULL, 0, NX_WRITEONLY);
      NXPrintf (stream, PLUCK_ML_FILE_FORMAT, [positionField doubleValue],
                                           [attenuationField doubleValue],
                                        [durationFactorField doubleValue]);
      NXSaveToFile (stream, [savepanel filename]);
      NXCloseMemory (stream, NX_FREEBUFFER);

    NX_HANDLER

      [self showError:"I/O error -- Can't save to file `%s'"
                with:[savepanel filename]];

    NX_ENDHANDLER
  }
  return self;
}

- play:sender
{
  char path [MAXPATHLEN + 1];
  int  error;

  sprintf (path, TMP_FILE, NXUserName ());

  if ([self makeFile:path
            withFrequency:[frequencyField doubleValue]
            volume:[volumeField doubleValue]
            duration:[durationField doubleValue]
            position:[positionField doubleValue]
            attenuation:[attenuationField doubleValue]
            durationFactor:[durationFactorField doubleValue]]) {

    error = SNDPlaySoundfile (path, 0);

    if (error != SND_ERR_NONE)
      [self showError:"Sound error: %s" with:SNDSoundError (error)];
  }

  return self;
}

@end
