//
//  Lynkeos
//  $Id: MyCocoaFilesReader.m 578 2014-09-07 21:11:32Z j-etienne $
//
//  Created by Jean-Etienne LAMIAUD on Fri Mar 03 2005.
//  Copyright (c) 2005-2014. Jean-Etienne LAMIAUD
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program 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 General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
// 

/** The size by which the "time" array is enlarged. */
#define K_TIME_PAGE_SIZE 1024

#include <AppKit/NSAffineTransform.h>

#include <pthread.h>

#if !GNUSTEP
#include <QTKit/QTTrack.h>
#include <QTKit/QTMedia.h>
#endif

#include "processing_core.h"
#include "MyCachePrefs.h"

#include "MyCocoaFilesReader.h"

/** Mutex used to avoid a deadlock when drawing offscreen from many threads */
static pthread_mutex_t offScreenLock;

/** Common function to retrieve a sample from an NSImage. */
static void getImageSample( void * const * const sample, 
                            floating_precision_t precision, u_char nPlanes,
                            u_short atx, u_short aty, u_short atw, u_short ath, 
                            u_short width,
                            NSImage* image, LynkeosIntegerSize imageSize )
{
   NSRect cocoaRect;
   NSImageRep* srcImageRep;
   NSImage* offScreenImage;
   NSBitmapImageRep* bitMap;
   u_char *plane[5];
   u_short x, y, rowSize, pixelSize;
   BOOL planar;
   float color[3];

   if ( nPlanes <= 0 || nPlanes > 3 )
   {
      NSLog( @"Invalid number of planes %d", nPlanes );
      return;
   }
   if( image == nil )
      return;

   // Convert this rectangle to Cocoa coordinates system
   cocoaRect = NSMakeRect(atx,
                          imageSize.height - ath - aty,
                          atw,ath );

   // Create an image to draw the NSImage in
   offScreenImage = [[[NSImage alloc] initWithSize:cocoaRect.size] autorelease];
   pthread_mutex_lock( &offScreenLock );
   [offScreenImage lockFocus];

   NSAffineTransform * xform = [NSAffineTransform transform];
   [xform translateXBy: -cocoaRect.origin.x yBy: -cocoaRect.origin.y];
   [xform concat];

   srcImageRep = [image bestRepresentationForRect:
                     NSMakeRect(0,0,imageSize.width,imageSize.height)
                                          context:nil
                                            hints:nil];
   // Force full pixel scale
   [srcImageRep drawInRect:NSMakeRect(0,0,imageSize.width,imageSize.height)];

   bitMap = [[[NSBitmapImageRep alloc] initWithFocusedViewRect:cocoaRect] 
                                                                   autorelease];
   [offScreenImage unlockFocus];
   pthread_mutex_unlock( &offScreenLock );

   // Access the data
   [bitMap getBitmapDataPlanes:plane];
   rowSize = [bitMap bytesPerRow];
   pixelSize = [bitMap bitsPerPixel]/8;

   planar = [bitMap isPlanar];

   for ( y = 0; y < ath; y++ )
   {
      for ( x = 0; x < atw; x++ )
      {
         u_short c;

         // Read the data in the bitmap
         if ( planar )
         {
            color[RED_PLANE] = plane[0][y*rowSize+x];
            color[GREEN_PLANE] = plane[1][y*rowSize+x];
            color[BLUE_PLANE] = plane[2][y*rowSize+x];
         }
         else
         {
            color[RED_PLANE] = plane[0][y*rowSize+x*pixelSize];
            color[GREEN_PLANE] = plane[0][y*rowSize+x*pixelSize+1];
            color[BLUE_PLANE] = plane[0][y*rowSize+x*pixelSize+2];
         }

         // Convert to monochrome, if needed
         if ( nPlanes == 1 )
            color[0] = (color[RED_PLANE] 
                        + color[GREEN_PLANE] 
                        + color[BLUE_PLANE]) / 3.0;

         // Fill in the sample buffer
         for( c = 0; c < nPlanes; c++ )
            SET_SAMPLE(sample[c],precision,x,y,width,color[c]);
      }
   }
}

@implementation MyCocoaImageReader

+ (void) initialize
{
   pthread_mutex_init( &offScreenLock, NULL );
}

+ (void) lynkeosFileTypes:(NSArray**)fileTypes
{
   *fileTypes = [NSImage imageFileTypes];
}

- (id) initWithURL:(NSURL*)url
{
   self = [super init];

   if ( self != nil )
   {
      NSImage *image;

      _path = [[url path] retain];

      image = [self getNSImage];
      if ( image != nil )
      {
         NSRect r = {{0.0, 0.0}, [image size]};
         NSImageRep *rep = [image bestRepresentationForRect:r
                                                    context:nil
                                                      hints:nil];

         _size = LynkeosMakeIntegerSize([rep pixelsWide],[rep pixelsHigh]);
      }
      else
      {
         [self release];
         self = nil;
      }
   }

   return( self );
}

- (void) dealloc
{
   [_path release];
   [super dealloc];
}

- (void) imageWidth:(u_short*)w height:(u_short*)h
{
   *w = _size.width;
   *h = _size.height;
}

// We deal only with RGB images
- (u_short) numberOfPlanes
{
   return( 3 );
}

- (void) getMinLevel:(double*)vmin maxLevel:(double*)vmax
{
   *vmin = 0.0;
   *vmax = 255.0;
}

- (NSImage*) getNSImage
{
   return( [[[NSImage alloc] initWithContentsOfFile: _path] autorelease] );
}

- (void) getImageSample:(void * const * const)sample 
          withPrecision:(floating_precision_t)precision
             withPlanes:(u_short)nPlanes
                    atX:(u_short)x Y:(u_short)y W:(u_short)w H:(u_short)h
              lineWidth:(u_short)width
{
   getImageSample( sample, precision, nPlanes, x, y, w, h, width, 
                   [self getNSImage], _size );
}

- (NSDictionary*) getMetaData
{
   return( nil );
}

@end

#if !defined GNUSTEP
/*!
 * @abstract ObjC container for a CVPixelBuffer
 */
@interface MyPixelBufferContainer : NSObject
{
@public
   CVPixelBufferRef _pixbuf; //!< The pixel buffer we wrap around
}
//! Dedicated initializer
- (id) initWithPixelBuffer:(CVPixelBufferRef)pixbuf ;
@end

@implementation MyPixelBufferContainer
- (id) initWithPixelBuffer:(CVPixelBufferRef)pixbuf
{
   if ( (self = [self init]) != nil )
   {
      _pixbuf = pixbuf;
      CVPixelBufferRetain( _pixbuf );
   }
   return( self );
}

- (void) dealloc
{
   CVPixelBufferRelease(_pixbuf);
   [super dealloc];
}
@end

NSDictionary *imageReadingOptions = nil;

static void pixelFormatCharacteristics( OSType pixelFormat,
                                  u_short *nPlanes,
                                  u_short *nSamples,
                                  u_short *nBits,
                                  BOOL *bigEndian,
                                  BOOL *hasAlpha,
                                  BOOL *alphaFirst)
{
   // Provide the characteristics of the supported formats
   if ( pixelFormat == kCVPixelFormatType_24RGB )
   {
      *nPlanes = 3;
      *nSamples = 3;
      *nBits = 8;
      *bigEndian = YES;
      *hasAlpha = NO;
      *alphaFirst = NO;
   }
   else if ( pixelFormat == kCVPixelFormatType_24BGR )
   {
      *nPlanes = 3;
      *nSamples = 3;
      *nBits = 8;
      *bigEndian = NO;
      *hasAlpha = NO;
      *alphaFirst = NO;
   }
   else if ( pixelFormat == kCVPixelFormatType_32ARGB )
   {
      *nPlanes = 3;
      *nSamples = 4;
      *nBits = 8;
      *bigEndian = YES;
      *hasAlpha = YES;
      *alphaFirst = YES;
   }
   else if ( pixelFormat == kCVPixelFormatType_32BGRA )
   {
      *nPlanes = 3;
      *nSamples = 4;
      *nBits = 8;
      *bigEndian = NO;
      *hasAlpha = YES;
      *alphaFirst = YES;
   }
   else if ( pixelFormat == kCVPixelFormatType_32RGBA )
   {
      *nPlanes = 3;
      *nSamples = 4;
      *nBits = 8;
      *bigEndian = YES;
      *hasAlpha = YES;
      *alphaFirst = NO;
   }
   else if ( pixelFormat == kCVPixelFormatType_32ABGR )
   {
      *nPlanes = 3;
      *nSamples = 4;
      *nBits = 8;
      *bigEndian = NO;
      *hasAlpha = YES;
      *alphaFirst = NO;
   }
   else if ( pixelFormat == kCVPixelFormatType_64ARGB )
   {
      *nPlanes = 3;
      *nSamples = 4;
      *nBits = 16;
      *bigEndian = YES;
      *hasAlpha = YES;
      *alphaFirst = YES;
   }
   else if ( pixelFormat == kCVPixelFormatType_48RGB )
   {
      *nPlanes = 3;
      *nSamples = 3;
      *nBits = 8;
      *bigEndian = YES;
      *hasAlpha = NO;
      *alphaFirst = NO;
   }
   else if ( pixelFormat == kCVPixelFormatType_32AlphaGray )
   {
      *nPlanes = 1;
      *nSamples = 2;
      *nBits = 16;
      *bigEndian = YES;
      *hasAlpha = YES;
      *alphaFirst = YES;
   }
   else if ( pixelFormat == kCVPixelFormatType_16Gray )
   {
      *nPlanes = 1;
      *nSamples = 1;
      *nBits = 16;
      *bigEndian = YES;
      *hasAlpha = NO;
      *alphaFirst = NO;
   }
   // Unsupported format
   else
   {
      *nPlanes = 0;
      *nSamples = 0;
      *nBits = 0;
      *bigEndian = NO;
      *hasAlpha = NO;
      *alphaFirst = NO;
   }
}

/*!
 * @abstract Private methods of MyQuickTimeReader
 */
@interface MyQuickTimeReader(Private)
//! Get a pixel buffer for the specified image, in cache or in the movie
- (CVPixelBufferRef) getPixelBufferAtIndex:(u_long)index ;
@end

@implementation MyQuickTimeReader(Private)
- (CVPixelBufferRef) getPixelBufferAtIndex:(u_long)index
{
   NSString *key = [_url stringByAppendingFormat:@"&%06ld",index];
   LynkeosObjectCache *movieCache = [LynkeosObjectCache movieCache];
   MyPixelBufferContainer *pix;
   CVPixelBufferRef img;
   NSError *err;

   if ( movieCache != nil &&
        (pix=(MyPixelBufferContainer*)[movieCache getObjectForKey:key]) != nil )
   {
      CVPixelBufferRetain( pix->_pixbuf );
      return( pix->_pixbuf );
   }

   NSAssert( index < _imageNumber, @"Access outside the movie" );

   // Go to the required time
   // Try to avoid skipping frames because of multiprocessing
   BOOL doSkip = ( movieCache == nil
                   || index < _currentImage
                   || index > (_currentImage+numberOfCpus+1) );
   do
   {
      if ( index != _currentImage )
      {
         if ( doSkip )
            _currentImage = index;
         else
            _currentImage++;  // This fills the cache with images in between
      }

      // And get the pixel buffer content
      img = (CVPixelBufferRef)[_movie frameImageAtTime:_times[_currentImage]
                                        withAttributes:imageReadingOptions
                                                 error:&err];
      if ( img == NULL )
         NSLog( @"Error reading movie image, %@", err != nil ? [err localizedDescription] : @"" );

      else if ( movieCache != nil )
         [movieCache setObject:
            [[[MyPixelBufferContainer alloc] initWithPixelBuffer:img] autorelease]
                        forKey:[_url stringByAppendingFormat:@"&%06ld",_currentImage]];
   } while ( index != _currentImage );

   if ( img != NULL )
      CVPixelBufferRetain(img);

   return( img );
}
@end

@implementation MyQuickTimeReader

+ (void) load
{
   // Nothing to do, this is just to force the runtime to load this class
}

+ (void) initialize
{
   imageReadingOptions = [[NSDictionary dictionaryWithObjectsAndKeys:
                          QTMovieFrameImageTypeCVPixelBufferRef,
                          QTMovieFrameImageType,
                          nil] retain];
}

+ (void) lynkeosFileTypes:(NSArray**)fileTypes
{
   NSArray *cocoaTypes = [QTMovie movieUnfilteredFileTypes];
   NSEnumerator *cocoaList = [cocoaTypes objectEnumerator];
   NSString *fileType;

   *fileTypes = [NSMutableArray array];
   while ( (fileType = [cocoaList nextObject]) != nil )
   {
      if ( [fileType caseInsensitiveCompare:@"mov"] == NSOrderedSame )
         [(NSMutableArray*)*fileTypes addObject:[NSNumber numberWithInt:1]];
      [(NSMutableArray*)*fileTypes addObject:fileType];
   }
}

- (id) initWithURL:(NSURL*)url
{
   NSError *qtErr = nil;
   u_long arraySize = K_TIME_PAGE_SIZE;
   CVPixelBufferRef img;

   if ( (self = [self init]) == nil )
      return( self );

   // Initialize the variables and open the movie
   _qtLock = [[NSLock alloc] init];
   _times = (QTTime*)malloc( arraySize*sizeof(QTTime) );
   _imageNumber = 0;
   _movie = [[QTMovie alloc] initWithAttributes:
               [NSDictionary dictionaryWithObjectsAndKeys:
                  url, QTMovieURLAttribute,
                  [NSNumber numberWithBool:YES],
                  QTMovieOpenForPlaybackAttribute,
                  [NSNumber numberWithBool:NO],
                  QTMovieOpenAsyncOKAttribute,
                  nil]
                                          error:&qtErr];
   if ( _movie == nil )
   {
      NSLog( @"Error creating QTMovie, %@",
             qtErr != nil ? [qtErr localizedDescription] : @"" );
      [self release];
      return( nil );
   }

   [_movie gotoBeginning];

   _url = [[url absoluteString] retain];

   // Extract the first image and check its characteristics
   img = (CVPixelBufferRef)[_movie frameImageAtTime:[_movie currentTime]
                                     withAttributes:imageReadingOptions
                                              error:&qtErr];
   if ( img == NULL )
   {
      NSLog( @"Error reading movie characteristic image, %@",
             qtErr != nil ? [qtErr localizedDescription] : @"" );
      [self release];
      return( nil );
   }

   BOOL pixmapAlphaFirst;
   OSType pixFormat = CVPixelBufferGetPixelFormatType( img );
   pixelFormatCharacteristics(pixFormat,
                              &_nPlanes,
                              &_pixmapSamples,
                              &_bitsPerPixel,
                              &_pixmapBigEndian,
                              &_pixmapHasAlpha,
                              &pixmapAlphaFirst);
   _pixmapFormat = (pixmapAlphaFirst ? NSAlphaFirstBitmapFormat : 0);

   if ( _nPlanes == 0 )
   {
      NSLog( @"Unsupported pixel format, %08X", pixFormat );
      [self release];
      return( nil );
   }

   _size = LynkeosIntegerSizeFromNSSize(
              [[_movie attributeForKey:QTMovieNaturalSizeAttribute] sizeValue]);

   // Extract the time of each frame
   QTTime movieTime = [_movie currentTime];
   QTTime previousTime;
   do
   {
      previousTime = movieTime;

      if ( _imageNumber >= arraySize )
      {
         arraySize += K_TIME_PAGE_SIZE;
         _times = (QTTime*)realloc( _times, 
                                    arraySize*sizeof(QTTime) );
      }

      _times[_imageNumber] = movieTime;
      _imageNumber++;

      [_movie stepForward];
      movieTime = [_movie currentTime];
   } while( QTTimeCompare(movieTime, previousTime) == NSOrderedDescending );

   _currentImage = _imageNumber;

   if ( _imageNumber == 0 )
   {
      NSLog( @"No image found in movie %@", url );
      [self release];
      self = nil;
   }

   QTTime duration = [_movie duration];
   if ( QTTimeCompare(duration, movieTime) == NSOrderedDescending )
   {
      // The reader failed to read all the movie, let FFMpeg try do to better
      NSLog( @"Failed to read entire movie %@", url );
      [self release];
      self = nil;
   }

   return( self );
}

- (void) dealloc
{
   free( _times );
   [_qtLock release];
   [_movie release];
   [_url release];

   [super dealloc];
}

- (void) imageWidth:(u_short*)w height:(u_short*)h
{
   *w = _size.width;
   *h = _size.height;
}

// We deal only with RGB images
- (u_short) numberOfPlanes
{
   return( _nPlanes );
}

- (void) getMinLevel:(double*)vmin maxLevel:(double*)vmax
{
   *vmin = 0.0;
   *vmax = 255.0;
}

- (u_long) numberOfFrames
{
   return( _imageNumber );
}

- (NSImage*) getNSImageAtIndex:(u_long)index
{
   // Quicktime operations are not thread safe
   [_qtLock lock];

   CVPixelBufferRef img = [self getPixelBufferAtIndex:index];

   // Make a NSImage with that data
   NSBitmapImageRep *bmap =
      [[[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
                                               pixelsWide:_size.width
                                               pixelsHigh:_size.height
                                            bitsPerSample:_bitsPerPixel
                                          samplesPerPixel:_pixmapSamples
                                                 hasAlpha:_pixmapHasAlpha
                                                 isPlanar:NO
                                           colorSpaceName:NSCalibratedRGBColorSpace
                                             bitmapFormat:_pixmapFormat
                                              bytesPerRow:CVPixelBufferGetBytesPerRow(img)
                                             bitsPerPixel:32]
       autorelease];


   if ( bmap != nil )
   {
      CVPixelBufferLockBaseAddress(img,0);
      memcpy( [bmap bitmapData], CVPixelBufferGetBaseAddress(img),
             CVPixelBufferGetBytesPerRow(img)*_size.height );
      CVPixelBufferUnlockBaseAddress(img,0);

      CVPixelBufferRelease(img);
   }
   else
      NSLog(@"Could not allocate a suitable bitmap");

   [_qtLock unlock];

   NSImage *image =
      [[[NSImage alloc] initWithSize:NSSizeFromIntegerSize(_size)] autorelease];
   [image addRepresentation:bmap];

   return( image );
}

- (void) getImageSample:(void * const * const)sample atIndex:(u_long)index
          withPrecision:(floating_precision_t)precision
             withPlanes:(u_short)nPlanes
                    atX:(u_short)x Y:(u_short)y W:(u_short)w H:(u_short)h
              lineWidth:(u_short)width
{
   // Quicktime operations are not thread safe
   [_qtLock lock];

   CVPixelBufferRef img = [self getPixelBufferAtIndex:index];
   u_short dx, dy, p, rowSize;
   u_short colorIndex[3];
   float color[3];
   void *plane;

   if ( CVPixelBufferIsPlanar(img) )
   {
      [_qtLock unlock];
      NSAssert( NO, @"The graphic context should not be planar" );
   }

   rowSize = CVPixelBufferGetBytesPerRow(img)*8/_bitsPerPixel;

   for (p = 0; p < _nPlanes; p++)
   {
      colorIndex[p] = p;
      if ( (_pixmapFormat & NSAlphaFirstBitmapFormat) != 0 )
         colorIndex[p]++;
      if ( !_pixmapBigEndian )
         colorIndex[p] = _pixmapSamples - colorIndex[p];
   }

   CVPixelBufferLockBaseAddress(img,0);
   plane = CVPixelBufferGetBaseAddress(img);
   for ( dy = 0; dy < h; dy++ )
   {
      for ( dx = 0; dx < w; dx++ )
      {
         for( p = 0; p < _nPlanes; p++ )
         {
            // Read the data in the bitmap
            if ( _bitsPerPixel == 8 )
               color[p] = ((u_char*)plane)[(y+dy)*rowSize+(x+dx)*_pixmapSamples+colorIndex[p]];
            else
               color[p] =
                     ((u_short*)plane)[(y+dy)*rowSize+(x+dx)*_pixmapSamples+colorIndex[p]]/256.0;
         }

         // Convert to monochrome, if needed
         if ( nPlanes == 1 && _nPlanes != 1 )
         {
            float c = 0;
            for( p = 0; p < _nPlanes; p++ )
               c += color[p];
            color[0] = c / _nPlanes;
         }

         // Fill in the sample buffer
         for( p = 0; p < nPlanes; p++ )
         {
            if ( p < _nPlanes )
            {
               SET_SAMPLE(sample[p],precision,dx,dy,width,color[p]);
            }
            else
            {
               SET_SAMPLE(sample[p],precision,dx,dy,width,0.0);
            }
         }
      }
   }
   CVPixelBufferUnlockBaseAddress(img,0);

   CVPixelBufferRelease(img);
   [_qtLock unlock];
}

- (NSDictionary*) getMetaData
{
   return( nil );
}

@end
#endif
