/*  HBPreviewGenerator.m $

 This file is part of the HandBrake source code.
 Homepage: <http://handbrake.fr/>.
 It may be used under the terms of the GNU General Public License. */
//

#import "HBPreviewGenerator.h"
#import "HBUtilities.h"
#import "Controller.h"

typedef enum EncodeState : NSUInteger {
    EncodeStateIdle,
    EncodeStateWorking,
    EncodeStateCancelled,
} EncodeState;

@interface HBPreviewGenerator ()

@property (nonatomic, readonly, retain) NSMutableDictionary *picturePreviews;
@property (nonatomic, readonly) NSUInteger imagesCount;
@property (nonatomic, readonly) hb_handle_t *handle;
@property (nonatomic, readonly) hb_title_t *title;

@property (nonatomic) hb_handle_t *privateHandle;
@property (nonatomic) NSTimer *timer;
@property (nonatomic) EncodeState encodeState;

@property (nonatomic, retain) NSURL *fileURL;

@end

@implementation HBPreviewGenerator

- (id) initWithHandle: (hb_handle_t *) handle andTitle: (hb_title_t *) title
{
    self = [super init];
    if (self)
    {
        _handle = handle;
        _title = title;
        _picturePreviews = [[NSMutableDictionary alloc] init];
        _imagesCount = [[[NSUserDefaults standardUserDefaults] objectForKey:@"PreviewsNumber"] intValue];
    }
    return self;
}

#pragma mark -
#pragma mark Preview images

/**
 * Returns the picture preview at the specified index
 *
 * @param index picture index in title.
 */
- (NSImage *) imageAtIndex: (NSUInteger) index shouldCache: (BOOL) cache
{
    if (index >= self.imagesCount)
        return nil;

    // The preview for the specified index may not currently exist, so this method
    // generates it if necessary.
    NSImage *theImage = [self.picturePreviews objectForKey:@(index)];

    if (!theImage)
    {
        theImage = [HBPreviewGenerator makeImageForPicture:index
                                                     libhb:self.handle
                                                     title:self.title
                                               deinterlace:self.deinterlace];
        if (cache && theImage)
            [self.picturePreviews setObject:theImage forKey:@(index)];
    }

    return theImage;
}

/**
 * Purges all images from the cache. The next call to imageAtIndex: will cause a new
 * image to be generated.
 */
- (void) purgeImageCache
{
    [self.picturePreviews removeAllObjects];
}

/** 
 * This function converts an image created by libhb (specified via pictureIndex) into
 * an NSImage suitable for the GUI code to use. If removeBorders is YES,
 * makeImageForPicture crops the image generated by libhb stripping off the gray
 * border around the content. This is the low-level method that generates the image.
 * -imageForPicture calls this function whenever it can't find an image in its cache.
 *
 * @param pictureIndex Index in title.
 * @param handle Handle to hb_handle_t.
 * @param title Handle to hb_title_t of desired title.
 * @param deinterlace Whether the preview image must be deinterlaced or not.
 */
+ (NSImage *) makeImageForPicture: (NSUInteger) pictureIndex
                            libhb: (hb_handle_t *) handle
                            title: (hb_title_t *) title
                      deinterlace: (BOOL) deinterlace
{
    NSImage *img = nil;

    hb_ui_geometry_t geo;
    geo.width = title->job->width;
    geo.height = title->job->height;
    // HBPreviewController will scale the image later,
    // ignore the par.
    geo.par.num = 1;
    geo.par.den = 1;
    memcpy(geo.crop, title->job->crop, sizeof(int[4]));

    hb_image_t *image;
    image = hb_get_preview2(handle, title->index, (int)pictureIndex, &geo, deinterlace);

    if (image)
    {
        // Create an NSBitmapImageRep and copy the libhb image into it, converting it from
        // libhb's format to one suitable for NSImage.

        // The image data returned by hb_get_preview2 is 4 bytes per pixel, BGRA format.
        // Alpha is ignored.
        NSBitmapImageRep *imgrep = [[[NSBitmapImageRep alloc]
                                     initWithBitmapDataPlanes:nil
                                     pixelsWide:image->width
                                     pixelsHigh:image->height
                                     bitsPerSample:8
                                     samplesPerPixel:3   // ignore alpha
                                     hasAlpha:NO
                                     isPlanar:NO
                                     colorSpaceName:NSCalibratedRGBColorSpace
                                     bitmapFormat:NSAlphaFirstBitmapFormat
                                     bytesPerRow:image->width * 4
                                     bitsPerPixel:32] autorelease];

        UInt8 *src_line = image->data;
        UInt32 *dst = (UInt32 *)[imgrep bitmapData];
        for (int r = 0; r < image->height; r++)
        {
            UInt32 *src = (UInt32 *)src_line;
            for (int c = 0; c < image->width; c++)
            {
#if TARGET_RT_LITTLE_ENDIAN
                *dst++ = Endian32_Swap(*src++);
#else
                *dst++ = *src++;
#endif
            }
            src_line += image->plane[0].stride;
        }

        img = [[[NSImage alloc] initWithSize: NSMakeSize(image->width, image->height)] autorelease];
        [img addRepresentation:imgrep];
    }

    hb_image_close(&image);

    return img;
}

#pragma mark -
#pragma mark Preview movie

+ (NSURL *) generateFileURLForType:(NSString *) type
{
    NSString *previewDirectory = [NSString stringWithFormat:@"%@/Previews/%d", [HBUtilities appSupportPath], getpid()];

    if (![[NSFileManager defaultManager] fileExistsAtPath:previewDirectory])
    {
        if (![[NSFileManager defaultManager] createDirectoryAtPath:previewDirectory
                                  withIntermediateDirectories:NO
                                                   attributes:nil
                                                        error:nil])
            return nil;
    }

    return [[NSURL fileURLWithPath:previewDirectory]
            URLByAppendingPathComponent:[NSString stringWithFormat:@"preview_temp.%@", type]];
}

/**
 * This function start the encode of a movie preview, the delegate will be
 * called with the updated the progress info and the fileURL.
 * The called must call HBController prepareJobForPreview before this.
 *
 * @param index picture index in title.
 * @param duration the duration in seconds of the preview movie.
 */
- (BOOL) createMovieAsyncWithImageIndex: (NSUInteger) index andDuration: (NSUInteger) duration;
{
    /* return if an encoding if already started */
    if (self.encodeState || index >= self.imagesCount)
        return NO;

    hb_job_t *job = self.title->job;

    /* Generate the file url and directories. */
    if (job->mux & HB_MUX_MASK_MP4)
    {
        /* we use .m4v for our mp4 files so that ac3 and chapters in mp4 will play properly */
        self.fileURL = [HBPreviewGenerator generateFileURLForType:@"m4v"];
    }
    else if (job->mux & HB_MUX_MASK_MKV)
    {
        self.fileURL = [HBPreviewGenerator generateFileURLForType:@"mkv"];
    }

    /* return if we couldn't get the fileURL */
    if (!self.fileURL)
        return NO;

    /* See if there is an existing preview file, if so, delete it */
    if (![[NSFileManager defaultManager] fileExistsAtPath:[self.fileURL path]])
    {
        [[NSFileManager defaultManager] removeItemAtPath:[self.fileURL path] error:NULL];
    }

    /* We now direct our preview encode to fileURL path */
    hb_job_set_file(job, [[self.fileURL path] UTF8String]);

    /* We use our advance pref to determine how many previews to scan */
    job->start_at_preview = (int)index + 1;
    job->seek_points = (int)self.imagesCount;
    job->pts_to_stop = duration * 90000LL;

    /* lets go ahead and send it off to libhb
     * Note: unlike a full encode, we only send 1 pass regardless if the final encode calls for 2 passes.
     * this should suffice for a fairly accurate short preview and cuts our preview generation time in half.
     * However we also need to take into account the indepth scan for subtitles.
     */

    int loggingLevel = [[[NSUserDefaults standardUserDefaults] objectForKey:@"LoggingLevel"] intValue];
    self.privateHandle = hb_init(loggingLevel, 0);

    /*
     * If scanning we need to do some extra setup of the job.
     */
    if (job->indepth_scan == 1)
    {
        char *encoder_preset_tmp  = job->encoder_preset  != NULL ? strdup(job->encoder_preset)  : NULL;
        char *encoder_tune_tmp    = job->encoder_tune    != NULL ? strdup(job->encoder_tune)    : NULL;
        char *encoder_options_tmp = job->encoder_options != NULL ? strdup(job->encoder_options) : NULL;
        char *encoder_profile_tmp = job->encoder_profile != NULL ? strdup(job->encoder_profile) : NULL;
        char *encoder_level_tmp   = job->encoder_level   != NULL ? strdup(job->encoder_level)   : NULL;
        /*
         * When subtitle scan is enabled do a fast pre-scan job
         * which will determine which subtitles to enable, if any.
         */
        hb_job_set_encoder_preset (job, NULL);
        hb_job_set_encoder_tune   (job, NULL);
        hb_job_set_encoder_options(job, NULL);
        hb_job_set_encoder_profile(job, NULL);
        hb_job_set_encoder_level  (job, NULL);
        job->pass = -1;
        hb_add(self.privateHandle, job);
        /*
         * reset the advanced settings
         */
        hb_job_set_encoder_preset (job, encoder_preset_tmp);
        hb_job_set_encoder_tune   (job, encoder_tune_tmp);
        hb_job_set_encoder_options(job, encoder_options_tmp);
        hb_job_set_encoder_profile(job, encoder_profile_tmp);
        hb_job_set_encoder_level  (job, encoder_level_tmp);
        free(encoder_preset_tmp);
        free(encoder_tune_tmp);
        free(encoder_options_tmp);
        free(encoder_profile_tmp);
        free(encoder_level_tmp);
    }

    /* Go ahead and perform the actual encoding preview scan */
    job->indepth_scan = 0;
    job->pass = 0;

    hb_add(self.privateHandle, job);

    /* we need to clean up the various lists after the job(s) have been set  */
    hb_job_reset(job);

    /* start the actual encode */
    self.encodeState = EncodeStateWorking;
    hb_system_sleep_prevent(self.privateHandle);

    [self startHBTimer];

    hb_start(self.privateHandle);

    return YES;
}

/**
 * Cancels the encoding process
 */
- (void) cancel
{
    if (self.privateHandle)
    {
        hb_state_t s;
        hb_get_state2(self.privateHandle, &s);

        if (self.encodeState && (s.state == HB_STATE_WORKING ||
                                  s.state == HB_STATE_PAUSED))
        {
            self.encodeState = EncodeStateCancelled;
            hb_stop(self.privateHandle);
            hb_system_sleep_allow(self.privateHandle);
        }
    }
}

- (void) startHBTimer
{
    if (!self.timer)
    {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5
                                                      target:self
                                                    selector:@selector(updateState)
                                                    userInfo:nil
                                                     repeats:YES];
    }
}

- (void) stopHBTimer
{
    [self.timer invalidate];
    self.timer = nil;
}

- (void) updateState
{
    hb_state_t s;
    hb_get_state(self.privateHandle, &s);

    switch( s.state )
    {
        case HB_STATE_IDLE:
        case HB_STATE_SCANNING:
        case HB_STATE_SCANDONE:
            break;

        case HB_STATE_WORKING:
        {
			NSMutableString *info = [NSMutableString stringWithFormat: @"Encoding preview:  %.2f %%", 100.0 * s.param.working.progress];

			if( s.param.working.seconds > -1 )
            {
                [info appendFormat:@" (%.2f fps, avg %.2f fps, ETA %02dh%02dm%02ds)",
                 s.param.working.rate_cur, s.param.working.rate_avg, s.param.working.hours,
                 s.param.working.minutes, s.param.working.seconds];
            }

            double progress = 100.0 * s.param.working.progress;

            [self.delegate updateProgress:progress info:info];

            break;
        }

        case HB_STATE_MUXING:
        {
            NSString *info = @"Muxing Preview…";
            double progress = 100.0;

            [self.delegate updateProgress:progress info:info];

            break;
        }

        case HB_STATE_PAUSED:
            break;

        case HB_STATE_WORKDONE:
        {
            [self stopHBTimer];

            // Delete all remaining jobs since libhb doesn't do this on its own.
            hb_job_t * job;
            while( ( job = hb_job(self.privateHandle, 0) ) )
                hb_rem( self.handle, job );

            hb_system_sleep_allow(self.privateHandle);
            hb_stop(self.privateHandle);
            hb_close(&_privateHandle);
            self.privateHandle = NULL;

            /* Encode done, call the delegate and close libhb handle */
            if (self.encodeState != EncodeStateCancelled)
            {
                [self.delegate didCreateMovieAtURL:self.fileURL];
            }
            else
            {
                [self.delegate didCancelMovieCreation];
            }

            self.encodeState = EncodeStateIdle;

            break;
        }
    }
}

#pragma mark -

- (void) dealloc
{
    [_timer invalidate];
    [_timer release];
    _timer = nil;

    if (_privateHandle) {
        hb_system_sleep_allow(self.privateHandle);
        hb_stop(_privateHandle);
        hb_close(&_privateHandle);
    }

    [_fileURL release];
    _fileURL = nil;
    [_picturePreviews release];
    _picturePreviews = nil;

    [super dealloc];
}

@end
