//
//  HBAudioController.m
//  HandBrake
//
//  Created on 2010-08-24.
//

#import "HBAudioController.h"
#import "Controller.h"
#import "HBAudio.h"
#import "HBAudioDefaults.h"
#import "HBAudioDefaultsController.h"
#import "HBAudioTrackPreset.h"
#import "hb.h"
#include "lang.h"

NSString *keyAudioTrackIndex = @"keyAudioTrackIndex";
NSString *keyAudioTrackName = @"keyAudioTrackName";
NSString *keyAudioInputBitrate = @"keyAudioInputBitrate";
NSString *keyAudioInputSampleRate = @"keyAudioInputSampleRate";
NSString *keyAudioInputCodec = @"keyAudioInputCodec";
NSString *keyAudioInputCodecParam = @"keyAudioInputCodecParam";
NSString *keyAudioInputChannelLayout = @"keyAudioInputChannelLayout";
NSString *keyAudioTrackLanguageIsoCode = @"keyAudioTrackLanguageIsoCode";

NSString *HBMixdownChangedNotification = @"HBMixdownChangedNotification";


@interface HBAudioController () {
    IBOutlet NSTableView         * fTableView;

    NSMutableArray               * audioArray;        // the configured audio information
    NSArray                      * masterTrackArray;  // the master list of audio tracks from the title
    NSDictionary                 * noneTrack;         // this represents no audio track selection
}

@property (assign) IBOutlet NSPopUpButton *trackPopup;
@property (assign) IBOutlet NSButton *configureDefaults;
@property (assign) IBOutlet NSButton *reloadDefaults;
@property (nonatomic, readwrite) BOOL enabled;

@property (nonatomic, readwrite, retain) NSArray *masterTrackArray;
@property (nonatomic, retain) NSNumber *videoContainerTag; // initially is the default HB_MUX_MP4

// Defaults
@property (nonatomic, readwrite, retain) HBAudioDefaultsController *defaultsController;
@property (nonatomic, readwrite, retain) HBAudioDefaults *settings;

@end

@implementation HBAudioController

#pragma mark -
#pragma mark Accessors

@synthesize masterTrackArray;
@synthesize noneTrack;
@synthesize videoContainerTag;

- (instancetype)init
{
    self = [super initWithNibName:@"Audio" bundle:nil];
    if (self)
    {
        [self setVideoContainerTag: @HB_MUX_MP4];
        audioArray = [[NSMutableArray alloc] init];

        /* register that we are interested in changes made to the video container */
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserver: self selector: @selector(containerChanged:) name: HBContainerChangedNotification object: nil];
        [center addObserver: self selector: @selector(titleChanged:) name: HBTitleChangedNotification object: nil];
    }
    return self;
}

- (void) dealloc

{
    [[NSNotificationCenter defaultCenter] removeObserver: self];
    [masterTrackArray release];
    [noneTrack release];
    [audioArray release];
    [self setVideoContainerTag: nil];
    [super dealloc];
}

- (IBAction)addAllAudioTracks:(id)sender
{
    [self addTracksFromDefaults:YES];
}

- (IBAction)removeAll:(id)sender
{
    [self _clearAudioArray];
    [self switchingTrackFromNone:nil];
}

- (void)setUIEnabled:(BOOL)flag
{
    self.enabled = flag;
    [fTableView setEnabled:flag];
    [self.trackPopup setEnabled:flag];
    [self.configureDefaults setEnabled:flag];
    [self.reloadDefaults setEnabled:flag];
}

- (BOOL)validateUserInterfaceItem:(id < NSValidatedUserInterfaceItem >)anItem
{
    return self.enabled;
}

#pragma mark -
#pragma mark HBController Support

- (NSArray *)audioTracks

{
    NSMutableArray *tracksArray = [NSMutableArray array];

    NSUInteger audioArrayCount = [self countOfAudioArray];
    for (NSUInteger counter = 0; counter < audioArrayCount; counter++)
    {
        HBAudio *anAudio = [self objectInAudioArrayAtIndex: counter];
        if ([anAudio enabled])
        {
            NSMutableDictionary *dict = [NSMutableDictionary dictionary];
            NSNumber *sampleRateToUse = ([anAudio.sampleRate[keyAudioSamplerate] intValue] == 0 ?
                                         anAudio.track[keyAudioInputSampleRate] :
                                         anAudio.sampleRate[keyAudioSamplerate]);

            dict[@"Track"]            = @([anAudio.track[keyAudioTrackIndex] intValue] -1);
            dict[@"TrackDescription"] = anAudio.track[keyAudioTrackName];
            dict[@"Encoder"]          = anAudio.codec[keyAudioCodecName];
            dict[@"Mixdown"]          = anAudio.mixdown[keyAudioMixdownName];
            dict[@"Samplerate"]       = anAudio.sampleRate[keyAudioSampleRateName];
            dict[@"Bitrate"]          = anAudio.bitRate[keyAudioBitrateName];

            // output is not passthru so apply gain
            if (!([[anAudio codec][keyAudioCodec] intValue] & HB_ACODEC_PASS_FLAG))
            {
                dict[@"TrackGainSlider"] = anAudio.gain;
            }
            else
            {
                // output is passthru - the Gain dial is disabled so don't apply its value
                dict[@"TrackGainSlider"] = @0;
            }

            if (hb_audio_can_apply_drc([anAudio.track[keyAudioInputCodec] intValue],
                                       [anAudio.track[keyAudioInputCodecParam] intValue],
                                       [anAudio.codec[keyAudioCodec] intValue]))
            {
                dict[@"TrackDRCSlider"] = anAudio.drc;
            }
            else
            {
                // source isn't AC3 or output is passthru - the DRC dial is disabled so don't apply its value
                dict[@"TrackDRCSlider"] = @0;
            }

            dict[@"JobEncoder"]    = anAudio.codec[keyAudioCodec];
            dict[@"JobMixdown"]    = anAudio.mixdown[keyAudioMixdown];
            dict[@"JobSamplerate"] = sampleRateToUse;
            dict[@"JobBitrate"]    = anAudio.bitRate[keyAudioBitrate];

            [tracksArray addObject:dict];
        }
    }

    return tracksArray;
}

- (void) addTracksFromQueue: (NSArray *) queueArray

{
    // Reinitialize the configured list of audio tracks
    [self _clearAudioArray];

    // The following is the pattern to follow, but with Audio%dTrack being the key to seek...
    // Can we assume that there will be no skip in the data?
    for (NSDictionary *audioDict in queueArray)
    {
        HBAudio *newAudio = [[HBAudio alloc] init];
        [newAudio setController: self];
        [self insertObject: newAudio inAudioArrayAtIndex: [self countOfAudioArray]];
        [newAudio setVideoContainerTag: [self videoContainerTag]];
        [newAudio setTrackFromIndex: [audioDict[@"Track"] intValue] + 1];
        [newAudio setCodecFromName: audioDict[@"Encoder"]];
        [newAudio setMixdownFromName: audioDict[@"Mixdown"]];
        [newAudio setSampleRateFromName: audioDict[@"Samplerate"]];
        [newAudio setBitRateFromName: audioDict[@"Bitrate"]];
        [newAudio setDrc: audioDict[@"TrackDRCSlider"]];
        [newAudio setGain: audioDict[@"TrackGainSlider"]];
        [newAudio release];
    }

    [self switchingTrackFromNone: nil]; // see if we need to add one to the list
}

- (void)applySettingsFromPreset:(NSDictionary *)preset
{
    self.settings = [[[HBAudioDefaults alloc] init] autorelease];
    [self.settings applySettingsFromPreset:preset];
    [self.settings validateEncoderFallbackForVideoContainer:[self.videoContainerTag intValue]];

    [self addTracksFromDefaults:NO];
}

- (void) _clearAudioArray
{
    while (0 < [self countOfAudioArray])
    {
        [self removeObjectFromAudioArrayAtIndex: 0];
    }
}

/**
 *  Uses the templateAudioArray from the preset to create the audios for the specified trackIndex.
 *
 *  @param templateAudioArray the track template.
 *  @param trackIndex         the index of the source track.
 *  @param firstOnly          use only the first track of the template or all.
 */
- (void) _processPresetAudioArray: (NSArray *) templateAudioArray forTrack: (NSUInteger) trackIndex firstOnly: (BOOL) firstOnly

{
    for (HBAudioTrackPreset *preset in templateAudioArray)
    {
        BOOL fallenBack = NO;
        HBAudio *newAudio = [[HBAudio alloc] init];
        [newAudio setController: self];
        [self insertObject: newAudio inAudioArrayAtIndex: [self countOfAudioArray]];
        [newAudio setVideoContainerTag: [self videoContainerTag]];
        [newAudio setTrackFromIndex: (int)trackIndex];
        
        const char *name = hb_audio_encoder_get_name(preset.encoder);
        NSString *audioEncoder = nil;
        
        // Check if we need to use a fallback
        if (name)
        {
            audioEncoder = @(name);
            if (preset.encoder & HB_ACODEC_PASS_FLAG &&
                ![newAudio setCodecFromName:audioEncoder])
            {
                int passthru, fallback;
                fallenBack = YES;
                passthru   = hb_audio_encoder_get_from_name([audioEncoder UTF8String]);
                fallback   = hb_audio_encoder_get_fallback_for_passthru(passthru);
                name       = hb_audio_encoder_get_name(fallback);
                
                // If we couldn't find an encoder for the passthru format
                // fall back to the selected encoder fallback
                if (name == NULL)
                {
                    name = hb_audio_encoder_get_name(self.settings.encoderFallback);
                }
            }
            else
            {
                name = hb_audio_encoder_sanitize_name([audioEncoder UTF8String]);
            }
            audioEncoder = @(name);
        }
        
        // If our preset wants us to support a codec that the track does not support, instead
        // of changing the codec we remove the audio instead.
        if ([newAudio setCodecFromName:audioEncoder])
        {
            const char *mixdown = hb_mixdown_get_name(preset.mixdown);
            if (mixdown)
            {
                [newAudio setMixdownFromName: @(mixdown)];
            }
            
            const char *sampleRateName = hb_audio_samplerate_get_name(preset.sampleRate);
            if (!sampleRateName)
            {
                [newAudio setSampleRateFromName: @"Auto"];
            }
            else
            {
                [newAudio setSampleRateFromName: @(sampleRateName)];
            }
            if (!fallenBack)
            {
                [newAudio setBitRateFromName: [NSString stringWithFormat:@"%d", preset.bitRate]];
            }
            [newAudio setDrc: @(preset.drc)];
            [newAudio setGain: @(preset.gain)];
        }
        else
        {
            [self removeObjectFromAudioArrayAtIndex: [self countOfAudioArray] - 1];
        }
        [newAudio release];
        
        if (firstOnly)
        {
            break;
        }
    }
}

/**
 *  Matches the source audio tracks with the specific language iso code.
 *
 *  @param isoCode         the iso code to match.
 *  @param selectOnlyFirst whether to match only the first track for the iso code.
 *
 *  @return a NSIndexSet with the index of the matched tracks.
 */
- (NSIndexSet *) _tracksWithISOCode: (NSString *) isoCode selectOnlyFirst: (BOOL) selectOnlyFirst
{
    NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet];
    
    // We search for the prefix noting that our titles have the format %d: %s where the %s is the prefix
    [masterTrackArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        if (idx) // Note that we skip the "None" track
        {
            if ([isoCode isEqualToString:@"und"] ||  [obj[keyAudioTrackLanguageIsoCode] isEqualToString:isoCode])
            {
                [indexes addIndex:idx];
                
                if (selectOnlyFirst)
                {
                    *stop = YES;
                }
            }
        }
    }];

    return indexes;
}

- (void) _processPresetAudioArray: (NSArray *) templateAudioArray forTracks: (NSIndexSet *) trackIndexes firstOnly: (BOOL) firstOnly
{
    __block BOOL firsTrack = firstOnly;
    [trackIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
        // Add the track
        [self _processPresetAudioArray: self.settings.tracksArray forTrack:idx firstOnly:firsTrack];
        firsTrack = self.settings.secondaryEncoderMode ? YES : NO;
    }];
}

- (void) addTracksFromDefaults: (BOOL) allTracks

{
    BOOL firstTrack = NO;
    NSMutableIndexSet *tracksAdded = [NSMutableIndexSet indexSet];
    NSMutableIndexSet *tracksToAdd = [NSMutableIndexSet indexSet];

    // Reinitialize the configured list of audio tracks
    [self _clearAudioArray];

    if (self.settings.trackSelectionBehavior != HBAudioTrackSelectionBehaviorNone)
    {
        // Add tracks of Default and Alternate Language by name
        for (NSString *languageISOCode in self.settings.trackSelectionLanguages)
        {
            NSMutableIndexSet *tracksIndexes = [[self _tracksWithISOCode: languageISOCode
                                                 selectOnlyFirst: self.settings.trackSelectionBehavior == HBAudioTrackSelectionBehaviorFirst] mutableCopy];
            [tracksIndexes removeIndexes:tracksAdded];
            if (tracksIndexes.count)
            {
                [self _processPresetAudioArray:self.settings.tracksArray forTracks:tracksIndexes firstOnly:firstTrack];
                firstTrack = self.settings.secondaryEncoderMode ? YES : NO;
                [tracksAdded addIndexes:tracksIndexes];
            }
            [tracksIndexes release];
        }

        // If no preferred Language was found, add standard track 1
        if (tracksAdded.count == 0 && masterTrackArray.count > 1)
        {
            [tracksToAdd addIndex:1];
        }
    }

    // If all tracks should be added, add all track numbers that are not yet processed
    if (allTracks)
    {
        [tracksToAdd addIndexesInRange:NSMakeRange(1, masterTrackArray.count - 1)];
        [tracksToAdd removeIndexes:tracksAdded];
    }

    if (tracksToAdd.count)
    {
        [self _processPresetAudioArray:self.settings.tracksArray forTracks:tracksToAdd firstOnly:firstTrack];
    }

    // Add an None item
    [self switchingTrackFromNone: nil];
}

- (BOOL) anyCodecMatches: (int) aCodecValue

{
    BOOL retval = NO;
    NSUInteger audioArrayCount = [self countOfAudioArray];
    for (NSUInteger i = 0; i < audioArrayCount && !retval; i++)
    {
        HBAudio *anAudio = [self objectInAudioArrayAtIndex: i];
        if ([anAudio enabled] && aCodecValue == [[anAudio codec][keyAudioCodec] intValue])
        {
            retval = YES;
        }
    }
    return retval;
}

- (void) addNewAudioTrack

{
    HBAudio *newAudio = [[HBAudio alloc] init];
    [newAudio setController: self];
    [self insertObject: newAudio inAudioArrayAtIndex: [self countOfAudioArray]];
    [newAudio setVideoContainerTag: [self videoContainerTag]];
    [newAudio setTrack: noneTrack];
    [newAudio setDrc: @0.0f];
    [newAudio setGain: @0.0f];
    [newAudio release];
}

#pragma mark -
#pragma mark Notification Handling

- (void) settingTrackToNone: (HBAudio *) newNoneTrack

{
    // If this is not the last track in the array we need to remove it.  We then need to see if a new
    // one needs to be added (in the case when we were at maximum count and this switching makes it
    // so we are no longer at maximum.
    NSUInteger index = [audioArray indexOfObject: newNoneTrack];

    if (NSNotFound != index && index < [self countOfAudioArray] - 1)
    {
        [self removeObjectFromAudioArrayAtIndex: index];
    }
    [self switchingTrackFromNone: nil]; // see if we need to add one to the list
}

- (void) switchingTrackFromNone: (HBAudio *) noLongerNoneTrack

{
    NSUInteger count = [self countOfAudioArray];
    BOOL needToAdd = NO;

    // If there is no last track that is None we add one.
    if (0 < count)
    {
        HBAudio *lastAudio = [self objectInAudioArrayAtIndex: count - 1];
        if ([lastAudio enabled])
        {
            needToAdd = YES;
        }
    }
    else
    {
        needToAdd = YES;
    }

    if (needToAdd)
    {
        [self addNewAudioTrack];
    }
}

// This gets called whenever the video container changes.
- (void) containerChanged: (NSNotification *) aNotification

{
    NSDictionary *notDict = [aNotification userInfo];

    [self setVideoContainerTag: notDict[keyContainerTag]];

    // Update each of the instances because this value influences possible settings.
    for (HBAudio *audioObject in audioArray)
    {
        [audioObject setVideoContainerTag: [self videoContainerTag]];
    }

    // Update the Auto Passthru Fallback Codec Popup
    // lets get the tag of the currently selected item first so we might reset it later
    [self.settings validateEncoderFallbackForVideoContainer:[self.videoContainerTag intValue]];
}

- (void) titleChanged: (NSNotification *) aNotification

{
    NSDictionary *notDict = [aNotification userInfo];
    NSData *theData = notDict[keyTitleTag];
    hb_title_t *title = NULL;

    // Reinitialize the configured list of audio tracks
    [self _clearAudioArray];
    
    [theData getBytes: &title length: sizeof(title)];
    if (title)
    {
        hb_audio_config_t *audio;
        hb_list_t *list = title->list_audio;
        int i, count = hb_list_count(list);

        // Reinitialize the master list of available audio tracks from this title
        NSMutableArray *newTrackArray = [NSMutableArray array];
        [noneTrack release];
        noneTrack = [@{keyAudioTrackIndex: @0,
                       keyAudioTrackName: NSLocalizedString(@"None", @"None"),
                       keyAudioInputCodec: @0} retain];
        [newTrackArray addObject: noneTrack];
        for (i = 0; i < count; i++)
        {
            audio = (hb_audio_config_t *) hb_list_audio_config_item(list, i);
            [newTrackArray addObject: @{keyAudioTrackIndex: @(i + 1),
                                        keyAudioTrackName: [NSString stringWithFormat: @"%d: %s", i, audio->lang.description],
                                        keyAudioInputBitrate: @(audio->in.bitrate / 1000),
                                        keyAudioInputSampleRate: @(audio->in.samplerate),
                                        keyAudioInputCodec: [NSNumber numberWithUnsignedInteger: audio->in.codec],
                                        keyAudioInputCodecParam: [NSNumber numberWithUnsignedInteger: audio->in.codec_param],
                                        keyAudioInputChannelLayout: @(audio->in.channel_layout),
                                        keyAudioTrackLanguageIsoCode: @(audio->lang.iso639_2)}];
        }
        self.masterTrackArray = newTrackArray;
        [self switchingTrackFromNone: nil]; // this ensures there is a None track at the end of the list
    }
    else
    {
        self.masterTrackArray = nil;
    }

}

#pragma mark - Defaults

- (IBAction)showSettingsSheet:(id)sender
{
    self.defaultsController = [[[HBAudioDefaultsController alloc] initWithSettings:self.settings] autorelease];

	[NSApp beginSheet:[self.defaultsController window]
       modalForWindow:[[self view] window]
        modalDelegate:self
       didEndSelector:@selector(sheetDidEnd)
          contextInfo:NULL];
}

- (void)sheetDidEnd
{
    self.defaultsController = nil;
}

- (IBAction)reloadDefaults:(id)sender
{
    [self addTracksFromDefaults:NO];
}

#pragma mark -
#pragma mark KVC

- (NSUInteger) countOfAudioArray

{
    return [audioArray count];
}

- (HBAudio *) objectInAudioArrayAtIndex: (NSUInteger) index

{
    return audioArray[index];
}

- (void) insertObject: (HBAudio *) audioObject inAudioArrayAtIndex: (NSUInteger) index;

{
    [audioArray insertObject: audioObject atIndex: index];
}

- (void) removeObjectFromAudioArrayAtIndex: (NSUInteger) index

{
    [audioArray removeObjectAtIndex: index];
}

@end

