#import "MovieView.h"

/*
 * Movie 2.51 - 5/7/92 pjf
 *
 * Differences between 2.5 and 2.51:
 * - the save: method actually has a prayer of working when the
 *   user saves on top of an existing movie and the old copy can't be renamed.
 *
 * Differences between 2.0 and 2.5:
 * - buttons to control cache depth
 *
 * - turn off multiframe .tiffs by default (define BC_VERSION_1 if you
 *   have a .tiff movie and are too lazy to use tiffutil to turn
 *   it into an .anim directory)
 *
 * - now able to save movie (currently-selected depth)
 *
 */

@implementation MovieView

- initFrame:(const NXRect *) frameRect
{
  const char *x;
  [(self = [super initFrame:frameRect]) allocateGState];
  state = STOPPED;
  mode = LOOP;
  maxw = maxh = -1.0;
  movieFrame=NULL;
  anim = nil;
  pingDuringDisplay=NO;
  x=NXGetDefaultValue("Movie","DefaultDepth");
  if (!x) dmode=D_DEF; /* use default depth */
  else switch(atoi(x)) {
    default:
    case 0: dmode=D_DEF; break;
    case 2: dmode=D_2; break;
    case 8: dmode=D_8; break;
    case 12: dmode=D_12; break;
    case 24: dmode=D_24; break;
    };
  updateControls=NO;
  return self;
}

- drawSelf:(const NXRect *) rects :(int)count
{
  NXImage *image;
  NXPoint origin = {0.0,0.0};
  
  if (!movieFrame) return nil;
  image = movieFrame[currentFrameIndex].image;
  if (!image) return self;
  [image composite:NX_COPY toPoint:&origin];
  if (pingDuringDisplay) NXPing();
  if ((currentFrameIndex == 0) ||
      ((mode == BOUNCE)&&(currentFrameIndex == numFrames-1))) {
    if (state != STOPPED) {
      double t=[anim getDoubleRealTime]+[anim getSyncTime];
      double afps=numFrames/(t-oldt);
      [actualFpsText setDoubleValue:afps];
      oldt=t;
      }
    }
  if (updateControls) {
    [fNumSlider setIntValue:currentFrameIndex];
    [fNumText setIntValue:currentFrameIndex];
    };
  return self;
}


- (BOOL)open:sender
{
#ifdef BC_VERSION_1
  const char *const types[] = { "tiff", "anim", (const char *) NULL };
#else
  const char *const types[] = { "anim", (const char *) NULL };
#endif
  id pan = [OpenPanel new];
  const char *const *filenames;
  char filename[FILENAME_MAX];

  if (![pan runModalForTypes:types]) return NO;
  if ((filenames = [pan filenames]) == NULL) return NO;
  sprintf(filename,"%s/%s", [pan directory], filenames[0]);
  strcpy(moviePath,filename);
  return [self openFile:filename];
}

- setUpWindowStuff:(char *)filename;
{
  char ptitle[FILENAME_MAX];
  Window *w=[self window];
  char *ptr=rindex(filename,'/')+1;
  if (ptr == (char *)1) ptr=filename;
  sprintf(ptitle,"Controls for %s",ptr);
  [panel setTitle:ptitle];
  [panel setNextResponder:w];
  [w sizeWindow:maxw:maxh]; /* will recache images */
  [w setTitleAsFilename:filename];
  [w setMiniwindowIcon:"movieDoc.tiff"];

  [[fNumSlider setMaxValue:numFrames-1] setEnabled:YES];
  [nFramesText setIntValue:numFrames-1];

  [depthButtons selectCellAt:(int)dmode:0];
  [panel orderFront:self];
  [w makeKeyAndOrderFront:self];
  [self display];
  return self;
}

// openFile: returns YES if the frames were successfully read, NO if not.
- (BOOL)openFile:(char *)filename
{
  char *ptr=rindex(filename,'.');
  List *bitmaps;

  if (!ptr) {
    NXRunAlertPanel(NULL,"Impossible filename %s",NULL,NULL,NULL,filename);
    return NO;
    };

  /* get the bitmaps */
  if (!strcmp(ptr,".anim")) {  /* the file is an Icon-style .anim directory */
    char buf[FILENAME_MAX];
    char *ptr2;
    *ptr='\0'; /* clobber extension */
    ptr2=1+rindex(filename,'/'); /* danger danger */
    if (ptr2 == (char *)1) ptr2=filename; /* if not /full/path/name */
    sprintf(buf,"%s.anim/%s",filename,ptr2);
    bitmaps = [self openAnimDirectory:buf];
    }
#ifdef BC_VERSION_1
  else if (!strcmp(ptr,".tiff")) { /* a slew o' TIFFs in one file */
    bitmaps = [NXBitmapImageRep newListFromFile:filename];
    }
#endif
  else { /* this shouldn't happen */
    NXRunAlertPanel(NULL,"Impossible filename %s",NULL,NULL,NULL,filename);
    return NO;
    };

  if (!bitmaps) {
    NXRunAlertPanel(NULL,"Couldn't get bitmaps from %s",NULL,NULL,NULL,
                    filename);
    return NO;
    };

  [self allocateFrames:bitmaps];
  [bitmaps free]; /* does not free elements */
  [self setUpWindowStuff:filename];
  return YES;
}


- (List *)openAnimDirectory:(char *)filenameRoot
{
  List *bitmaps = [[List alloc] init];
  int i=1;
  while (1) {
    char buf[FILENAME_MAX];
    NXBitmapImageRep *newbitmap;
    sprintf(buf,"%s.%d.tiff",filenameRoot,i++);
    if ((access(buf,R_OK)) == -1) break;
    newbitmap = [[NXBitmapImageRep alloc] initFromFile:buf];
    if (!newbitmap) {
      NXRunAlertPanel(NULL,"Couldn't get bitmap from %s",NULL,NULL,NULL,
                    buf);
      [[bitmaps freeObjects] free];
      return nil;
      }
    else
      [bitmaps addObject:newbitmap];
    };
  return bitmaps;
}

NXWindowDepth deps[] = {
  NX_DefaultDepth, NX_TwoBitGrayDepth,
  NX_EightBitGrayDepth, NX_TwelveBitRGBDepth,
  NX_TwentyFourBitRGBDepth /*,NX_PurinaCatChow__ChowChowChowDepth*/
  };

// set up Frame data structures and find max frame size
- allocateFrames:(List *)frames
{
  int i;
  numFrames=[frames count];
  NX_MALLOC(movieFrame,movieFrameStruct,numFrames);
  for(i=0;i<numFrames;i++) {
    NXImage *nxi;
    NXBitmapImageRep *bm=[frames objectAt:i];
    NXSize sz;
    [bm getSize:&sz];
    movieFrame[i].original=bm;
    nxi=movieFrame[i].image=[[NXImage alloc] initSize:&sz];
    [nxi setUnique:YES]; /* make caches disjoint */
    [nxi setBackgroundColor:NX_COLORBLACK];
    /* keep track of largest frame */
    if (sz.width > maxw) maxw=sz.width;
    if (sz.height > maxh) maxh=sz.height;
    };
  return self;
}

/*****************************************************************
 *****************************************************************/
- superviewSizeChanged:(NXSize *)old
{
  [anim stopEntry];
  [super superviewSizeChanged:old];
  [self recache];
  [self renderFrames];
  [self display];
  [anim resetRealTime];
  [anim startEntry];
  return self;
}

- renderFrames
{
  int i;
  NXRect r;
  [self getBounds:&r];
  for(i=0;i<numFrames;i++)
    if ([movieFrame[i].image lockFocus]) {
      [movieFrame[i].original drawIn:&r];
      [movieFrame[i].image unlockFocus];
      }
    else {
      fprintf(stderr,"Barf: couldn't lockFocus on image %d\n",
              movieFrame[i].image);
      abort();
      };
  return self;
}

- recache
// assume depth & size both changed 
//
// Appkit bug?  Can one render down from 24 bit color to 2 bit gray?
//
{
  NXRect r;
  int i;
  [self getBounds:&r];
  [self freeCaches];
  for(i=0;i<numFrames;i++) {
    movieFrame[i].image=[[NXImage alloc] initSize:&r.size];
    [movieFrame[i].image useCacheWithDepth:deps[(int)dmode]];
    };
  return self;
}

- save:sender
{
  const char *type = "anim"; // will only save in .anim format.
  SavePanel *sp = [SavePanel new];
  [sp setDelegate:self];
  [sp setRequiredFileType:type];
  if ([sp runModal]) { // OK was hit
    int i;
    char cwd[MAXPATHLEN];
    /* if directory exists, rename it with a wiggle in back. */
    if (access([sp filename],F_OK) == 0) {
      /* I could do this with a couple of calls to system(), but noooo,
       * I had to do it the had way. yeccch. */
      char *buf=malloc(strlen([sp filename]+2));
      sprintf(buf,"%s~",[sp filename]);
      if (!getwd(cwd)) {
        NXRunAlertPanel("FATAL","Couldn't get current directory.",NULL,NULL,NULL);
        abort();
        };
      if (rename([sp filename],buf) == -1) {
        // sledgehammer time.
        struct direct *de;
        DIR *dp;
        chdir([sp filename]);
        dp=opendir(".");
        while(de=readdir(dp)) unlink(de->d_name);
        closedir(dp);
        chdir(cwd);
        unlink([sp filename]);
        };
      };
    mkdir([sp filename],0755);
    chdir([sp filename]);
    for(i=0;i<numFrames;i++) {
      char buf3[MAXPATHLEN];
      char buf2[MAXPATHLEN];
      char *ptr;
      int fd;
      NXStream *s;
      strcpy(buf3,[sp filename]);
      ptr=rindex(buf3,'/')+1;
      *(rindex(ptr,'.'))='\0';
      sprintf(buf2,"./%s.%d.tiff",ptr,i+1);
      fd=open(buf2,O_WRONLY|O_CREAT,0644);
      s=NXOpenFile(fd,NX_WRITEONLY);
      [movieFrame[i].image writeTIFF:s];
      NXClose(s);
      close(fd);
      };
    chdir(cwd);
    };
  return self;
}

- (BOOL) panelValidateFilenames:sender
{
  if (!strcmp([sender filename],moviePath)) {
    NXRunAlertPanel("Save","Cannot overwrite original movie",NULL,NULL,NULL);
    return NO;
    };
  return YES;
}

- freeCaches
{
  int i;
  for(i=0;i<numFrames;i++) [movieFrame[i].image free];
  return self;
}

- freeOriginals
{
  int i;
  for(i=0;i<numFrames;i++) [movieFrame[i].original free];
  return self;
}

- free
{
  [self freeCaches];
  [self freeOriginals];
  [self freeGState];
  [anim free];
  anim=nil;
  return [super free];
}

- copy:sender
{
  char *buffer;
  NXStream *stream;
  int length, maxLength;
  Pasteboard *pasteboard = [Pasteboard new];
  runState s=state;
  [anim stopEntry];
  if (s!=STOPPED) [self stop:self];
  [pasteboard declareTypes:&NXPostScriptPboardType num:1 owner:self];
  stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
  [self copyPSCodeInside:&bounds to:stream];
  NXFlush(stream);
  NXGetMemoryBuffer(stream, &buffer, &length, &maxLength);
  [pasteboard writeType:NXPostScriptPboard data:buffer length:length];
  NXCloseMemory(stream, NX_FREEBUFFER);
  switch(s) {
    case STOPPED: break;
    case FORWARD: 
    case REVERSE: [anim startEntry]; break;
    };
  return self;
}

- tick:sender
{
  int end=(state == FORWARD) ? numFrames-1 : 0;

  switch(mode) {
    case ONCE: {
      if (currentFrameIndex == end) {
        [self stop:self];
        return self;
        }
      else
        currentFrameIndex += (int)state;
      break; /* not needed */
      };
    case LOOP: {
      int next = currentFrameIndex + (int)state;
      if (((state == FORWARD)&&(next>end)) ||
          ((state == REVERSE)&&(next<end))) {
        currentFrameIndex = (state < 0) ? numFrames-1 : 0;
        }
      else
        currentFrameIndex = next;
      break;
      };
    case BOUNCE: {
      int next = currentFrameIndex + (int)state;
      if (((state == FORWARD)&&(next>end)) ||
          ((state == REVERSE)&&(next<end))) {
        if (state == FORWARD) [self selectStateButton:REV];
        if (state == REVERSE) [self selectStateButton:FWD];
        state *= -1;
        currentFrameIndex += (int)state;
        }
      else
        currentFrameIndex = next;
      break;
      };
    };
  [self display];
  return self;
}

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

- fwd:sender
{

  if (numFrames < 2) return self;
  if (state != STOPPED) [self stop:self];
  state = FORWARD;
  [self move:sender];
  return self;
}

- rev:sender
{
  if (numFrames < 2) return self;
  if (state != STOPPED) [self stop:self];
  state = REVERSE;
  [self move:sender];
  return self;
}

- move:sender
{
  double period = 1.0/[fpsSlider floatValue];
  if (numFrames < 2) { /* duh */
    [self selectStateButton:STOP];
    return self;
    };
  anim = [[Animator alloc] initChronon:period adaptation:0.05 /*?*/
                           target:self action:@selector(tick:)
                           autoStart:YES eventMask:0];
  if (state == FORWARD) [self selectStateButton:FWD];
  if (state == REVERSE) [self selectStateButton:REV];
  [fNumText setStringValue:""];
  [fNumSlider setEnabled:NO];
  oldt=[anim getSyncTime];
  return self;
}

- stop:sender
{

  if (numFrames < 2) return self;
  switch(state) {
    case FORWARD:
    case REVERSE: [anim free]; anim=nil;
    case STOPPED: break;
    }
  state = STOPPED;
  [self selectStateButton:STOP];
  [fNumText setIntValue:currentFrameIndex];
  [fNumSlider setEnabled:YES];
  [fNumSlider setIntValue:currentFrameIndex];
  return self;
}

- fwdStep:sender
{
  [self step:(int) FORWARD];
  return self;
}

- revStep:sender
{
  [self step:(int) REVERSE];
  return self;
}

- step:(int) direction
{
  if (numFrames < 2) return self;
  if (state != STOPPED) [self stop:self];
  if ((currentFrameIndex = (currentFrameIndex + direction) % numFrames) < 0)
    currentFrameIndex = numFrames + currentFrameIndex;
  [self selectStateButton:STOP];
  [fNumText setIntValue:currentFrameIndex];
  [fNumSlider setEnabled:YES];
  [fNumSlider setIntValue:currentFrameIndex];
  return [self display];
}

- expand2x:sender
{
  NXRect r;
  [self getBounds:&r];
  r.size.width *= 2.0;
  r.size.height *= 2.0;
  [[self window] sizeWindow:r.size.width :r.size.height];
  return self;
}

- reduce50pct:sender
{
  NXRect r;
  [self getBounds:&r];
  r.size.width *= 0.5;
  r.size.height *= 0.5;
  [[self window] sizeWindow:r.size.width :r.size.height];
  return self;
}

- restore:sender
{
  [[self window] sizeWindow:maxw :maxh];
  return self;
}


- modeButtonsChanged:sender
{
  mode = (runMode)[sender selectedRow];
  return self;
}

- fNumSliderChanged:sender
{
  if (currentFrameIndex == [sender intValue]) return self;
  currentFrameIndex=[sender intValue];
  [self stop:self];
  [self display];
  return self;
}

- fpsSliderChanged:sender
{
  [fpsText setFloatValue:[sender floatValue]];
  switch(state) {
    case FORWARD: 
    case REVERSE: {
      double period = 1.0/[fpsSlider floatValue];
      [anim free];
      anim = [[Animator alloc] initChronon:period adaptation:0.05
                               target:self action:@selector(tick:)
                               autoStart:YES eventMask:0];
      break;
      };
    case STOPPED: break;
    };
  return self;
}

- pingButtonChanged:sender
{
  switch([sender selectedRow]) {
    case 0: pingDuringDisplay=NO; break;
    case 1: pingDuringDisplay=YES; break;
    };
  return self;
}

- selectStateButton:(runState)b
{
  [stateButtons selectCellAt:0:((int)b)];
  return self;
}

- depthButtonsChanged:sender
{
  dmode=(depthMode)[sender selectedRow];
  [anim stopEntry];
  [self recache];
  [self renderFrames];
  [self display];
  [anim resetRealTime];
  [anim startEntry];
  return self;
}

- updateCheckBoxChanged:sender
{
  updateControls = !updateControls;
  return self;
}


// Window's delegate methods

- windowWillClose:sender
{
  [panel close];
  [self free];
  return self;
}

-windowDidMiniaturize:sender
{
  [panel orderOut:sender];
  [anim stopEntry];
  return self;
}

-windowDidDeminiaturize:sender
{
  [panel orderFront:sender];
  [anim resetRealTime];
  [anim startEntry];
  return self;
}
@end
